@zeplin/mcp-server 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,195 @@
1
+ # Zeplin MCP Server for AI-Assisted UI Implementation
2
+
3
+ This project implements a Model Context Protocol (MCP) server designed to assist developers in implementing UI screens and components directly from Zeplin designs. By providing Zeplin shortlinks (e.g., `https://zpl.io/...`) in your prompts to AI agents, this server fetches the necessary design specifications and asset details, enabling the models to generate corresponding code for your target framework.
4
+
5
+ The primary goal is to streamline the developer workflow by bridging the gap between design specifications in Zeplin and actual code implementation.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Features](#features)
10
+ - [Prerequisites](#prerequisites)
11
+ - [Installation](#installation)
12
+ - [Configuration](#configuration)
13
+ - [Development](#development)
14
+ - [Usage with MCP Clients (e.g., Cursor)](#usage-with-mcp-clients-eg-cursor)
15
+ - [Crafting Effective Prompts](#crafting-effective-prompts)
16
+ - [Example Prompt 1: Minor Changes/Additions](#example-prompt-1-minor-changesadditions)
17
+ - [Example Prompt 2: Complex Implementations (Component-First)](#example-prompt-2-complex-implementations-component-first)
18
+
19
+ ## Features
20
+
21
+ * **Seamless Zeplin Integration**: Fetch detailed design data for components and screens using simple Zeplin URLs (including `zpl.io` shortlinks).
22
+ * **Code Generation Ready**: Provides AI models with structured data for conciseness and relevance.
23
+ * **Asset Handling**: Automatically identifies and provides information for assets within designs. AI agents can then decide to download these assets (SVG, PNG, PDF, JPG) as needed for implementation.
24
+ * **Targeted Implementation**: Facilitates implementation of entire screens or individual components, including their variants, layers, and annotations, directly from Zeplin specifications.
25
+
26
+ ## Prerequisites
27
+
28
+ * [Node.js](https://nodejs.org/) (v20.x or later recommended)
29
+ * A Zeplin account.
30
+ * A Zeplin Personal Access Token (PAT). You can generate one from your Zeplin profile settings under "Developer" > "Personal access tokens". This token will need `read` permissions for the projects/styleguides you want to access.
31
+
32
+ ## Installation
33
+
34
+ 1. **Clone the repository:**
35
+ ```bash
36
+ git clone https://github.com/zeplin/zeplin-mcp.git
37
+ ```
38
+
39
+ 2. **Install dependencies:**
40
+ Using npm:
41
+ ```bash
42
+ npm install
43
+ ```
44
+
45
+ 3. **Build the project:**
46
+ The TypeScript code needs to be compiled to JavaScript. Assuming you have a build script in your `package.json` (e.g., `tsc` or `esbuild`):
47
+ ```bash
48
+ npm run build
49
+ ```
50
+
51
+ This will typically create a `dist` directory with the compiled JavaScript files (e.g., `dist/index.js`).
52
+
53
+ ## Configuration
54
+
55
+ Create a `.env` file in the root directory of the project with the following content:
56
+
57
+ ```bash
58
+ ZEPLIN_ACCESS_TOKEN=your_zeplin_personal_access_token
59
+ ```
60
+
61
+ Replace `your_zeplin_personal_access_token` with your actual Zeplin Personal Access Token.
62
+
63
+ ## Development
64
+
65
+ This project includes several npm scripts to help you with development:
66
+
67
+ ```bash
68
+ # Run TypeScript compiler in watch mode for development
69
+ npm run dev
70
+
71
+ # Build the project for production
72
+ npm run build
73
+
74
+ # Run ESLint on source files
75
+ npm run lint
76
+
77
+ # Automatically fix ESLint issues where possible
78
+ npm run lint:fix
79
+
80
+ # Test the MCP server locally with the inspector tool
81
+ npm run inspect
82
+ ```
83
+
84
+ ### Code Style and Linting
85
+
86
+ This project uses ESLint to enforce code quality and consistency. The configuration is in `eslint.config.js`. Key style guidelines include:
87
+
88
+ - 2 space indentation
89
+ - Double quotes for strings
90
+ - Semicolons required
91
+ - No trailing spaces
92
+ - Organized imports
93
+
94
+ When contributing to this project, please ensure your code follows these guidelines by running `npm run lint:fix` before submitting changes.
95
+
96
+ ## Usage with MCP Clients (e.g., Cursor)
97
+
98
+ To integrate this server with an MCP client like Cursor, you need to configure the client to connect to this server. Add the following to Cursor's `settings.json` (accessible via `Cmd/Ctrl + Shift + P` -> "Configure Language Specific Settings..." -> "JSON") or a similar configuration file for MCP providers:
99
+
100
+ ```jsonc
101
+ // In your MCP client's configuration (e.g., Cursor's settings.json)
102
+ {
103
+ // ... other configurations
104
+ "mcpServers": {
105
+ // ... other providers
106
+ "zeplin-mcp": {
107
+ "command": "node",
108
+ "args": [
109
+ "/path/to/your/zeplin-mcp/dist/index.js" // IMPORTANT: Update this path
110
+ ],
111
+ "env": {
112
+ "ZEPLIN_ACCESS_TOKEN": "<YOUR_ZEPLIN_PERSONAL_ACCESS_TOKEN>" // IMPORTANT: Replace with your actual token
113
+ }
114
+ }
115
+ // ...
116
+ }
117
+ // ...
118
+ }
119
+ ```
120
+
121
+ **Important:**
122
+ * Replace `"/path/to/your/zeplin-mcp/dist/index.js"` with the **absolute path** to the compiled `index.js` file in your `zeplin-mcp` project directory.
123
+ * Replace `<YOUR_ZEPLIN_PERSONAL_ACCESS_TOKEN>` with your actual Zeplin PAT.
124
+
125
+ ## Crafting Effective Prompts
126
+
127
+ The quality and specificity of your prompts significantly impact the AI's ability to generate accurate and useful code. These are not mandatory but will increase the output quality. Here are some examples to guide you:
128
+
129
+ ### Example Prompt 1: Minor Changes/Additions
130
+
131
+ When you need to implement a small update or addition to an existing screen or component based on a new Zeplin design version.
132
+
133
+ ```
134
+ The latest design for the following screen includes a new addition: a Checkbox component has been added to the MenuItem component, here is the short url of the screen <zeplin short url of the screen, e.g., https://zpl.io/abc123X>. Focus on the MenuItem component.
135
+
136
+ The Checkbox component can be found under the path/to/your/checkbox/component directory.
137
+ The relevant screen file is located at path/to/your/screen/file.tsx.
138
+ The MenuItem component, which needs to be modified, is located at path/to/your/menuitem/component.
139
+ Please implement this new addition.
140
+ ```
141
+
142
+ **Why this is effective:**
143
+ * **Contextualizes the change:** Clearly states what's new.
144
+ * **Provides the Zeplin link:** Allows the MCP server to fetch the latest design data.
145
+ * **Gives file paths:** Helps the AI locate existing code to modify.
146
+ * **Specifies components involved:** Narrows down the scope of work.
147
+
148
+ ### Example Prompt 2: Complex Implementations (Component-First)
149
+
150
+ For implementing larger screens or features, it's often best to build individual components first and then assemble them.
151
+
152
+ ```
153
+ Implement this component: <zeplin short url of the first component, e.g., https://zpl.io/def456Y>. Use Zeplin for design specifications.
154
+
155
+ (AI generates the first component...)
156
+
157
+ Implement this other component: <zeplin short url of the second component, e.g., https://zpl.io/ghi789Z>. Use Zeplin for design specifications.
158
+
159
+ (AI generates the second component...)
160
+
161
+ ...
162
+
163
+ Now, using the components you just implemented (and any other existing components), implement the following screen: <zeplin short url of the screen, e.g., https://zpl.io/jkl012A>. Use Zeplin for the screen layout and any direct elements.
164
+ ```
165
+
166
+ **Why this is effective:**
167
+
168
+ * **Breaks down complexity:** Tackles smaller, manageable pieces first.
169
+ * **Iterative approach:** Allows for review and correction at each step.
170
+ * **Builds on previous work:** The AI can use the components it just created.
171
+ * **Clear Zeplin references:** Ensures each piece is based on the correct design.
172
+
173
+
174
+
175
+ ### Strategies to deal with context window limitations
176
+
177
+ When dealing with complex Zeplin screens or components with many variants and layers, the amount of design data fetched can sometimes be extensive. This can potentially exceed the context window limitations of the AI model you are using, leading to truncated information or less effective code generation. Here are several strategies to manage the amount of information sent to the model:
178
+
179
+ 1. **Limit Screen Variants (`includeVariants: false`):**
180
+ * **How it works:** When using the `get_screen` tool, the model can be instructed to fetch only the specific screen version linked in the URL, rather than all its variants (e.g., different states, sizes, themes). This is done by setting the `includeVariants` parameter to `false` during the tool call.
181
+ * **When to use:** If your prompt is focused on a single specific version of a screen, or if the variants are not immediately relevant to the task at hand. This significantly reduces the amount of data related to variant properties and their respective layer structures.
182
+ * **Example Prompt Hint:** "Implement the login form from this screen: `https://zpl.io/abc123X`. I only need the specific version linked, not all its variants."
183
+ *The AI agent, when calling `get_screen`, should then ideally use `includeVariants: false`.*
184
+
185
+ 2. **Focus on Specific Layers/Components (`targetLayerName` or Targeted Prompts):**
186
+ * **How it works (using `targetLayerName`):** The `get_screen` tool has a `targetLayerName` parameter. If the model can identify a specific layer name from your prompt (e.g., "the 'Submit Button'"), it can use this parameter. The server will then return data primarily for that layer and its children, rather than the entire screen's layer tree.
187
+ * **How it works (Targeted Prompts):** Even without explicitly using `targetLayerName` in the tool call, very specific prompts can guide the model to internally prioritize or summarize information related to the mentioned element.
188
+ * **When to use:** When your task involves a specific part of a larger screen, like a single button, an icon, or a text block.
189
+ * **Example Prompt:** "Focus on the 'UserProfileHeader' component within this screen: `https://zpl.io/screenXYZ`. I need to implement its layout and text styles."
190
+ *If the AI uses `get_screen`, it could populate `targetLayerName: "UserProfileHeader"`.*
191
+
192
+ 3. **Iterative, Component-First Implementation:**
193
+ * **How it works:** As detailed in [Example Prompt 2: Complex Implementations (Component-First)](#example-prompt-2-complex-implementations-component-first), break down the implementation of a complex screen into smaller, component-sized tasks.
194
+ * **When to use:** For any non-trivial screen. This approach naturally limits the scope of each `get_component` or `get_screen` call to a manageable size.
195
+ * **Benefit:** Each request to the Zeplin MCP server will fetch a smaller, more focused dataset, making it easier to stay within context limits and allowing the model to concentrate on one piece at a time.
@@ -0,0 +1,74 @@
1
+ import { ZeplinApi, Configuration } from "@zeplin/sdk";
2
+ import { pruneLayersForTargetLayer } from "../utils/api-utils.js";
3
+ import { preProcess, preProcessDesignTokens } from "../utils/preprocessing.js";
4
+ /**
5
+ * Initialize the Zeplin API client
6
+ */
7
+ export const api = new ZeplinApi(new Configuration({ accessToken: process.env.ZEPLIN_ACCESS_TOKEN }));
8
+ /**
9
+ * Fetches design tokens for a project
10
+ * @param projectId The ID of the project
11
+ * @returns The design tokens data
12
+ */
13
+ export async function fetchProjectDesignTokens(projectId) {
14
+ const designTokens = await api.designTokens.getProjectDesignTokens(projectId, { includeLinkedStyleguides: true });
15
+ return {
16
+ designTokens: preProcessDesignTokens(designTokens.data),
17
+ };
18
+ }
19
+ /**
20
+ * Fetches design tokens for a styleguide
21
+ * @param styleguideId The ID of the styleguide
22
+ * @returns The design tokens data
23
+ */
24
+ export async function fetchStyleguideDesignTokens(styleguideId) {
25
+ const designTokens = await api.designTokens.getStyleguideDesignTokens(styleguideId, { includeLinkedStyleguides: true });
26
+ return {
27
+ designTokens: preProcessDesignTokens(designTokens.data),
28
+ };
29
+ }
30
+ /**
31
+ * Processes screen versions and annotations to create screen variants
32
+ * @param projectId The project ID
33
+ * @param screenIds Array of screen IDs
34
+ * @param variantNames Array of variant names
35
+ * @param targetLayerName Optional layer name to extract
36
+ * @returns Array of processed screen variants
37
+ */
38
+ export async function processScreenVersionsAndAnnotations(projectId, screenIds, variantNames, targetLayerName) {
39
+ const screenVersionResponses = await Promise.all(screenIds.map(async (screenId) => {
40
+ const response = await api.screens.getLatestScreenVersion(projectId, screenId);
41
+ const processedData = preProcess(response.data);
42
+ return {
43
+ ...response,
44
+ data: processedData
45
+ };
46
+ }));
47
+ const screenAnnotationsResponse = await Promise.all(screenIds.map((screenId) => api.screens.getScreenAnnotations(projectId, screenId)));
48
+ const screenAnnotations = screenAnnotationsResponse.map((response, index) => {
49
+ const annotations = response.data;
50
+ const screenVersion = screenVersionResponses[index];
51
+ return annotations.map((annotation) => ({
52
+ type: annotation.type.name,
53
+ text: annotation.content,
54
+ position: {
55
+ x: annotation.position.x * (screenVersion.data.width || 0),
56
+ y: annotation.position.y * (screenVersion.data.height || 0),
57
+ },
58
+ }));
59
+ });
60
+ return screenVersionResponses.map((response, index) => {
61
+ const screenVersion = response.data;
62
+ // If targetLayerName is provided, prune the layers.
63
+ // Otherwise, use all layers from the screenVersion.
64
+ const layersToInclude = targetLayerName
65
+ ? pruneLayersForTargetLayer(screenVersion.layers || [], targetLayerName)
66
+ : screenVersion.layers || [];
67
+ return {
68
+ name: variantNames[index],
69
+ annotations: screenAnnotations[index],
70
+ layers: layersToInclude,
71
+ assets: screenVersion.assets,
72
+ };
73
+ });
74
+ }
@@ -0,0 +1,61 @@
1
+ export const INSTRUCTIONS = `**Role:** You are an expert front-end developer tasked with generating code from a design specification provided as structured data.
2
+
3
+ **Goal:** Generate clean, maintainable, and accurate [Specify Target Framework/Language, e.g., React with Tailwind CSS / SwiftUI / HTML & CSS] code based on the provided **design specification (which could be a Zeplin Component or a Zeplin Screen)**.
4
+
5
+ **Inputs:**
6
+ 1. **Design Data (JSON):** This structured data represents either a Zeplin Component or a Zeplin Screen.
7
+ * For **Screens**, it typically includes an overall screen name, variants (if any), screen-level annotations, and a list of layers for each variant.
8
+ * For **Components**, it includes the component's name, its variants (each with potential \`props\` like \`variantProperties\` and \`layers\`), or a single component structure with its layers.
9
+ * Use this JSON for specific details: text \`content\`, \`component_name\` for identifying sub-components/instances, explicit color/typography values (to be mapped to tokens), layer structure, and \`annotations\`.
10
+
11
+ **Core Instructions:**
12
+
13
+ 1. **Determine Input Type:**
14
+ * First, analyze the top-level structure of the provided JSON. Determine if it represents a **Zeplin Component definition** (e.g., the JSON might have a top-level \`component\` object, or a \`name\` and \`variants\` array where variants have \`props\` or \`variantProperties\`) or a **Zeplin Screen definition** (e.g., the JSON describes a screen with its own \`name\`, \`variants\`, \`layers\`, and \`annotations\`).
15
+
16
+ 2. **If the Input is a Zeplin Component Definition:**
17
+ * Your primary goal is to generate the code that **defines this component**.
18
+ * The component definition should accept parameters/props based on any \`props\` or \`variantProperties\` found in the JSON for the component or its variants.
19
+ * If multiple \`variants\` are detailed for the component, the generated code should allow the component to render these different states, typically controlled via its props.
20
+ * The internal structure of the component will be derived from its \`layers\` (see "Processing Layers" below).
21
+
22
+ 3. **Processing Layers (applies when rendering a Screen OR defining the internal structure of a Component):**
23
+ * When iterating through the \`layers\` array found in the JSON (whether for a screen or for a component you are defining):
24
+ * **a. Component Instance Prioritization:**
25
+ * If a layer in the JSON has a \`component_name\` field, this indicates an instance of another component. **Strictly prioritize** using an existing component from the (optional) provided codebase context that matches this \`component_name\`.
26
+ * Extract necessary props for this component instance from its text content, styling, or nested layers.
27
+ * **Do NOT** generate new code for the children/internal structure of this identified component instance; assume the referenced component encapsulates that.
28
+ * If a matching component is not found in the codebase context, clearly note this (e.g., in comments) and generate a plausible placeholder or a basic structure based on its layer data.
29
+ * **b. Layout & Positioning:**
30
+ * Derive layout and structure from the **Layer Data JSON**.
31
+ * Use flexible layout techniques ([e.g., Flexbox, Grid, StackViews, AutoLayout]) appropriate for the target framework.
32
+ * **Avoid hardcoding pixel dimensions or absolute positions.** Use relative units, spacing tokens, or layout containers that adapt. Layer \`rect\` data in the JSON is a guide, not a rigid spec.
33
+ * **c. Styling:**
34
+ * **Design Tokens First:** When styling elements based on color (e.g., from a layer's fills like color: {r:38, g:43, b:46, a:1}) or typography (e.g., from textStyles like fontFamily: "Graphik", fontSize: 16, fontWeight: 500), your first priority is to find a matching design token.
35
+ * Consult Input JSON \`designTokens\`: Look for a token within the designTokens section of the provided input JSON. Match based on the value of the token. For example, if a layer has color: {r:38, g:43, b:46, a:1}, search for a color token whose value is rgb(38, 43, 46). If a text layer uses fontFamily: "Graphik", fontSize: 13, fontWeight: 500, search for a text style token whose value includes these font properties.
36
+ * Codebase Context (Fallback): If no direct match is found in the input JSON's designTokens, then attempt to map them to existing design tokens or variables from the provided codebase context.
37
+ * Raw Values (Last Resort): If no matching token is found in either the input JSON's designTokens or the codebase context, use the specific value from the JSON layer data but add a comment indicating a potential token is missing. For example:
38
+ // TODO: Use design token for color rgba(38, 43, 46, 1)
39
+ // TODO: Use design token for font: Graphik, 16px, 500w, letterSpacing 0.16
40
+ * **d. Content & Assets:**
41
+ * Use exact text \`content\` provided in the JSON data for labels, headings, paragraphs, etc.
42
+ * For images or icons identified in the JSON (e.g., via \`layer_name\` or \`component_name\` like "Omlet logo", "GitHub logo"), first search for them in the codebase and use them if they already exist.
43
+ * If not found, use the \`download_asset\` tool to download the relevant asset. The URL of the asset is located at the \`assets\` section of the JSON data. Use \`layer_source_id\` and \`layer_name\` to match which layers are using the asset.
44
+ * If the asset is not found in the codebase context and if the download fails as a last resort create a basic structure based on its layer data.
45
+
46
+ 4. **Annotations:**
47
+ * If the JSON includes an \`annotations\` field (either at the screen level or potentially associated with layers if the pre-processing step adds them there), treat its contents as **critical overrides or specific instructions** that MUST be followed, potentially contradicting other layers or visual representation.
48
+
49
+ 5. **Code Conventions & Brevity:**
50
+ * Follow existing naming conventions (variables, functions) if codebase context is provided. Otherwise, use clear, descriptive names.
51
+ * Be concise. Omit default HTML/CSS/framework attributes or styles. Generate only the necessary code to represent the design elements. Do not include boilerplate unless explicitly part of the component structure.
52
+
53
+ 6. **Output Format:**
54
+ * Generate code for [Specify Target: e.g., a single React functional component, a SwiftUI View struct, a block of HTML with associated CSS].
55
+ * If the input JSON was identified as a Zeplin Component definition, the output should be the code that **defines that component**, making it usable and configurable based on its properties and variants.
56
+ * If the input JSON was identified as a Zeplin Screen definition, the output should represent the **entire screen**, likely as a larger component or a composition of elements and other (potentially imported) components.
57
+ * Ensure the code is well-formatted and syntactically correct.
58
+
59
+ **Constraint:** Only use information present in the provided Design Data JSON, and any explicitly provided codebase context. Do not invent features or functionality not represented in the inputs.
60
+
61
+ **Now, analyze the provided Design Data (JSON), and generate the code according to these instructions.**`;
package/dist/index.js ADDED
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { api, fetchProjectDesignTokens, fetchStyleguideDesignTokens, processScreenVersionsAndAnnotations } from "./clients/zeplinApi.js";
6
+ import { INSTRUCTIONS } from "./constants.js";
7
+ import { assetRegistry } from "./utils/asset-registry.js";
8
+ import { getAssetUrl, downloadAsset } from "./utils/asset-utils.js";
9
+ import { createErrorResponse, createSuccessResponse } from "./utils/response-utils.js";
10
+ import { resolveUrl, URL_PATTERNS } from "./utils/url-utils.js";
11
+ /**
12
+ * Fetches and processes screen data from Zeplin
13
+ * @param url The resolved Zeplin screen URL
14
+ * @param includeVariants Whether to include variants in the response
15
+ * @param targetLayerName Optional name of layer to extract layer data for
16
+ * @returns Formatted response object with screen data or error message
17
+ */
18
+ export async function getScreenData(url, includeVariants, targetLayerName) {
19
+ const match = url.match(URL_PATTERNS.SCREEN);
20
+ if (!match) {
21
+ return createErrorResponse("Screen link is not valid — here's the expected format: https://app.zeplin.io/project/{projectId}/screen/{screenId}");
22
+ }
23
+ const [_, projectId, screenId] = match;
24
+ try {
25
+ // Reset the asset registry to clear any previous assets
26
+ assetRegistry.reset();
27
+ const screenResponse = await api.screens.getScreen(projectId, screenId);
28
+ const screen = screenResponse.data;
29
+ let name;
30
+ let screenIds;
31
+ let variantNames;
32
+ if (screen.variant && includeVariants) {
33
+ const variantGroupId = screen.variant.group.id;
34
+ const variantGroupResponse = await api.screens.getScreenVariant(projectId, variantGroupId);
35
+ const variantGroup = variantGroupResponse.data;
36
+ name = variantGroup.name;
37
+ screenIds = variantGroup.variants
38
+ .map((variant) => variant.screenId)
39
+ .filter((id) => id !== undefined);
40
+ variantNames = variantGroup.variants
41
+ .map((variant) => variant.value)
42
+ .filter((name) => name !== undefined);
43
+ }
44
+ else {
45
+ name = screen.name || "Unnamed Screen";
46
+ screenIds = [screenId];
47
+ variantNames = [screen.name || "Unnamed Screen"];
48
+ }
49
+ const variants = await processScreenVersionsAndAnnotations(projectId, screenIds, variantNames, targetLayerName);
50
+ // Register assets for future lookups but don't include them in the response
51
+ variants.forEach(variant => {
52
+ if (variant.assets && variant.assets.length > 0) {
53
+ assetRegistry.registerAssets(variant.assets.filter(asset => asset.contents));
54
+ }
55
+ });
56
+ const designTokens = await fetchProjectDesignTokens(projectId);
57
+ const screenData = {
58
+ type: "Screen",
59
+ name,
60
+ variants: variants.map(variant => ({
61
+ name: variant.name,
62
+ annotations: variant.annotations,
63
+ layers: variant.layers
64
+ // Assets are intentionally omitted
65
+ })),
66
+ designTokens: designTokens.designTokens
67
+ };
68
+ return createSuccessResponse(screenData, INSTRUCTIONS);
69
+ }
70
+ catch (error) {
71
+ return createErrorResponse(`Failed to fetch screen data: ${error instanceof Error ? error.message : String(error)}`);
72
+ }
73
+ }
74
+ /**
75
+ * Fetches and processes component data from Zeplin
76
+ * @param url The resolved Zeplin component URL
77
+ * @returns Formatted response object with component data or error message
78
+ */
79
+ export async function getComponentData(url) {
80
+ let match = url.match(URL_PATTERNS.COMPONENT);
81
+ let isProjectComponent = false;
82
+ if (!match) {
83
+ match = url.match(URL_PATTERNS.PROJECT_STYLEGUIDE_COMPONENT);
84
+ isProjectComponent = true;
85
+ }
86
+ if (!match) {
87
+ return createErrorResponse("Component link is not valid. Expected formats: https://app.zeplin.io/styleguide/{styleguideId}/component/{componentId} or https://app.zeplin.io/project/{projectId}/styleguide/component/{componentId}");
88
+ }
89
+ try {
90
+ assetRegistry.reset();
91
+ let componentResponse;
92
+ let styleguideId;
93
+ let projectId;
94
+ let componentId;
95
+ if (isProjectComponent) {
96
+ [, projectId, componentId] = match;
97
+ componentResponse = await api.components.getProjectComponent(projectId, componentId, { includeLatestVersion: true });
98
+ }
99
+ else {
100
+ [, styleguideId, componentId] = match;
101
+ componentResponse = await api.components.getStyleguideComponent(styleguideId, componentId, { includeLatestVersion: true });
102
+ }
103
+ const designTokens = isProjectComponent && projectId
104
+ ? await fetchProjectDesignTokens(projectId)
105
+ : styleguideId
106
+ ? await fetchStyleguideDesignTokens(styleguideId)
107
+ : undefined;
108
+ const component = componentResponse.data;
109
+ const sectionId = component.section?.id;
110
+ // Register assets for future lookups but don't include them in the response
111
+ if (component.latestVersion?.assets) {
112
+ assetRegistry.registerAssets(component.latestVersion.assets.filter(asset => asset.contents));
113
+ }
114
+ if (!sectionId || !styleguideId) {
115
+ const sanitizedComponent = JSON.parse(JSON.stringify(component));
116
+ if (sanitizedComponent.latestVersion && "assets" in sanitizedComponent.latestVersion) {
117
+ delete sanitizedComponent.latestVersion.assets;
118
+ }
119
+ const response = { component: sanitizedComponent };
120
+ return createSuccessResponse(response, INSTRUCTIONS);
121
+ }
122
+ const sectionsResponse = await api.components.getStyleguideComponentSections(styleguideId);
123
+ const sections = sectionsResponse.data;
124
+ const section = sections.find((s) => s.id === sectionId);
125
+ if (!section) {
126
+ const sanitizedComponent = JSON.parse(JSON.stringify(component));
127
+ if (sanitizedComponent.latestVersion && "assets" in sanitizedComponent.latestVersion) {
128
+ delete sanitizedComponent.latestVersion.assets;
129
+ }
130
+ const response = { component: sanitizedComponent };
131
+ return createSuccessResponse(response, INSTRUCTIONS);
132
+ }
133
+ const sectionComponentsResponse = await api.components.getStyleguideComponents(styleguideId, {
134
+ sectionId,
135
+ includeLatestVersion: true,
136
+ });
137
+ const sectionComponents = sectionComponentsResponse.data;
138
+ sectionComponents.forEach(componentVariant => {
139
+ if (componentVariant.latestVersion?.assets) {
140
+ assetRegistry.registerAssets(componentVariant.latestVersion.assets.filter(asset => asset.contents));
141
+ }
142
+ });
143
+ const componentData = {
144
+ name: section.name,
145
+ variants: sectionComponents.map((componentVariant) => {
146
+ return {
147
+ name: componentVariant.name,
148
+ props: componentVariant.variantProperties?.map((property) => ({
149
+ name: property.name,
150
+ value: property.value,
151
+ })),
152
+ layers: componentVariant.latestVersion?.layers,
153
+ // Assets are intentionally omitted
154
+ };
155
+ }),
156
+ designTokens: designTokens?.designTokens,
157
+ };
158
+ return createSuccessResponse(componentData, INSTRUCTIONS);
159
+ }
160
+ catch (error) {
161
+ return createErrorResponse(`Failed to fetch component data: ${error instanceof Error ? error.message : String(error)}`);
162
+ }
163
+ }
164
+ // Initialize the MCP server
165
+ const server = new McpServer({
166
+ name: "Zeplin MCP Server",
167
+ version: "0.1.0",
168
+ });
169
+ // Register the get_component tool
170
+ server.tool("get_component", "Fetches detailed design specifications for a specific Zeplin component, including its properties, variants, layers, and associated design tokens. Use this when you need to understand the structure and styling of a single, reusable UI element from Zeplin.", {
171
+ url: z.string().url(),
172
+ }, async ({ url }) => {
173
+ try {
174
+ const resolvedUrl = await resolveUrl(url.trim());
175
+ return await getComponentData(resolvedUrl);
176
+ }
177
+ catch (error) {
178
+ return createErrorResponse(`Error resolving or processing URL: ${error instanceof Error ? error.message : String(error)}`);
179
+ }
180
+ });
181
+ // Register the get_screen tool
182
+ server.tool("get_screen", "Fetches detailed design data for a specific screen from Zeplin. This includes screen variants, layer information (structure, position, styling), annotations, and project-level design tokens. Use this to understand screen layout, content, and interactions for development or review.", {
183
+ url: z.string().url(),
184
+ includeVariants: z.boolean().default(true)
185
+ .describe("Set to `true` (default) to retrieve all variants of the screen (e.g., different states or sizes). Set to `false` if only the specific screen version linked in the URL is needed, or to conserve tokens if variants are not relevant to the user's query. Fetching all variants provides a complete picture but uses more tokens."),
186
+ targetLayerName: z.string().optional()
187
+ .describe("Optional. If the user's query refers to a specific named layer or element on the screen (e.g., 'the submit button', 'user profile image'), provide that layer's exact name here. This will focus the returned data on that specific layer and its children, making the response more concise and relevant. If omitted or the layer name is not found, data for all layers on the screen will be returned."),
188
+ }, async ({ url, includeVariants, targetLayerName }) => {
189
+ try {
190
+ const resolvedUrl = await resolveUrl(url.trim());
191
+ return await getScreenData(resolvedUrl, includeVariants, targetLayerName);
192
+ }
193
+ catch (error) {
194
+ return createErrorResponse(`Error resolving or processing URL: ${error instanceof Error ? error.message : String(error)}`);
195
+ }
196
+ });
197
+ // Register the download_layer_asset tool
198
+ server.tool("download_layer_asset", "Downloads a specific visual asset (e.g., SVG icon, PNG image) for a given layer from Zeplin and saves it to a local path. Use this tool when an asset referenced in the design (obtained from `get_screen` or `get_component`) is missing from the codebase and needs to be fetched directly from Zeplin.", {
199
+ layerSourceId: z.string()
200
+ .describe("The unique source ID of the layer for which the asset should be downloaded. This ID is obtained from the `layers` array in the response of `get_screen` or `get_component` calls, from a `sourceId` or similar field associated with a specific layer that has exportable assets"),
201
+ localPath: z.string()
202
+ .describe("The absolute path to the directory where images/assets are stored in the project. If the directory does not exist, it will be created. The format of this path should respect the directory format of the operating system you are running on. Don't use any special character escaping in the path name either."),
203
+ assetType: z.enum(["svg", "png", "pdf", "jpg"])
204
+ .describe("The desired format of the asset to download. Must be one of 'svg', 'png', 'jpg', or 'pdf'. Choose the format most suitable for the project's needs or as indicated by design specifications. If unsure, 'svg' is often preferred for vector graphics and 'png' for bitmaps."),
205
+ }, async ({ layerSourceId, localPath, assetType }) => {
206
+ const assetUrl = getAssetUrl(layerSourceId, assetType);
207
+ if (!assetUrl) {
208
+ return createErrorResponse(`No asset found with layer source ID: ${layerSourceId} and format ${assetType}`);
209
+ }
210
+ return await downloadAsset(assetUrl, localPath);
211
+ });
212
+ server.tool("get_design_tokens", "Download design tokens for a project or styleguide", {
213
+ resourceId: z.string()
214
+ .describe("The ID of the project or styleguide for which the design tokens should be downloaded."),
215
+ }, async ({ resourceId }) => {
216
+ let projectDesignTokens = null;
217
+ let styleguideDesignTokens = null;
218
+ let projectError = null;
219
+ let styleguideError = null;
220
+ try {
221
+ projectDesignTokens = await fetchProjectDesignTokens(resourceId);
222
+ }
223
+ catch (error) {
224
+ projectError = error;
225
+ }
226
+ try {
227
+ styleguideDesignTokens = await fetchStyleguideDesignTokens(resourceId);
228
+ }
229
+ catch (error) {
230
+ styleguideError = error;
231
+ }
232
+ // If both failed, return an error
233
+ if (!projectDesignTokens && !styleguideDesignTokens) {
234
+ const errorMessage = `No design tokens found for project or styleguide with ID: ${resourceId}`;
235
+ if (projectError && styleguideError) {
236
+ return createErrorResponse(`${errorMessage}. Project error: ${projectError instanceof Error ? projectError.message : String(projectError)}. Styleguide error: ${styleguideError instanceof Error ? styleguideError.message : String(styleguideError)}`);
237
+ }
238
+ return createErrorResponse(errorMessage);
239
+ }
240
+ return createSuccessResponse({
241
+ projectDesignTokens,
242
+ styleguideDesignTokens,
243
+ }, INSTRUCTIONS);
244
+ });
245
+ // Start the server
246
+ try {
247
+ const transport = new StdioServerTransport();
248
+ await server.connect(transport);
249
+ }
250
+ catch (error) {
251
+ console.error(`Failed to start MCP server: ${error instanceof Error ? error.message : String(error)}`);
252
+ process.exit(1);
253
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Recursively searches for a component with the given name in the layer tree
3
+ * and returns the layer, its parent, and path to the component
4
+ * @param layers Array of layers to search through
5
+ * @param targetLayerName Name of the component to find
6
+ * @param parentLayer Optional parent layer reference
7
+ * @param path Optional path to the current position in the tree
8
+ * @returns Object containing found status, layer, parent layer, and path
9
+ */
10
+ export function findTargetLayer(layers, targetLayerName, parentLayer = null, path = []) {
11
+ if (!layers || !Array.isArray(layers)) {
12
+ return { found: false, layer: null, parentLayer: null, path: [] };
13
+ }
14
+ for (const layer of layers) {
15
+ if (layer.componentName === targetLayerName || layer.name === targetLayerName) {
16
+ return { found: true, layer, parentLayer, path: [...path, layer] };
17
+ }
18
+ if (layer.layers && Array.isArray(layer.layers)) {
19
+ const result = findTargetLayer(layer.layers, targetLayerName, layer, [...path, layer]);
20
+ if (result.found) {
21
+ return result;
22
+ }
23
+ }
24
+ }
25
+ return { found: false, layer: null, parentLayer: null, path: [] };
26
+ }
27
+ /**
28
+ * Prunes layers data to only include the target component, its immediate parent, and all of its children.
29
+ * @param layers The full layers array from a screen version.
30
+ * @param targetLayerName The name of the layer/component to find and keep.
31
+ * @returns Pruned layer array containing only relevant layers
32
+ */
33
+ export function pruneLayersForTargetLayer(layers, targetLayerName) {
34
+ if (!targetLayerName) {
35
+ return layers;
36
+ }
37
+ if (!Array.isArray(layers)) {
38
+ return [];
39
+ }
40
+ const { found, layer: targetLayer, parentLayer } = findTargetLayer(layers, targetLayerName);
41
+ if (!found || !targetLayer) {
42
+ return [];
43
+ }
44
+ if (parentLayer) {
45
+ return [parentLayer];
46
+ }
47
+ else {
48
+ return [targetLayer];
49
+ }
50
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Registry for assets that can be looked up by layerSourceId
3
+ */
4
+ class AssetRegistry {
5
+ assetRecords = new Map();
6
+ /**
7
+ * Resets the asset registry
8
+ */
9
+ reset() {
10
+ this.assetRecords.clear();
11
+ }
12
+ /**
13
+ * Registers an asset in the registry
14
+ * @param asset The asset to register
15
+ */
16
+ registerAsset(asset) {
17
+ if (!asset.layerSourceId || !asset.contents) {
18
+ return;
19
+ }
20
+ const record = {
21
+ displayName: asset.displayName,
22
+ layerName: asset.layerName,
23
+ contents: asset.contents.map(content => ({
24
+ format: content.format,
25
+ url: content.url
26
+ }))
27
+ };
28
+ this.assetRecords.set(asset.layerSourceId, record);
29
+ }
30
+ /**
31
+ * Registers multiple assets in the registry
32
+ * @param assets The assets to register
33
+ */
34
+ registerAssets(assets) {
35
+ assets.forEach(asset => this.registerAsset(asset));
36
+ }
37
+ /**
38
+ * Gets an asset record by its layer source ID
39
+ * @param layerSourceId The layer source ID
40
+ * @returns The asset record or undefined if not found
41
+ */
42
+ getAssetRecord(layerSourceId) {
43
+ return this.assetRecords.get(layerSourceId);
44
+ }
45
+ /**
46
+ * Gets the URL for a specific format of an asset
47
+ * @param layerSourceId The layer source ID
48
+ * @param format The desired format
49
+ * @returns The URL or undefined if not found
50
+ */
51
+ getAssetUrl(layerSourceId, format) {
52
+ const record = this.assetRecords.get(layerSourceId);
53
+ if (!record)
54
+ return undefined;
55
+ const content = record.contents.find(c => c.format === format);
56
+ return content?.url;
57
+ }
58
+ /**
59
+ * Extracts asset information from component, screen, or variant data
60
+ * @param data The data containing assets
61
+ */
62
+ extractAssetsFromData(data) {
63
+ if (!data)
64
+ return;
65
+ // Extract from variants
66
+ if (Array.isArray(data.variants)) {
67
+ data.variants.forEach((variant) => {
68
+ if (variant.assets && Array.isArray(variant.assets)) {
69
+ this.registerAssets(variant.assets.filter((asset) => asset.contents));
70
+ }
71
+ });
72
+ }
73
+ // Extract from component
74
+ if (data.component?.latestVersion?.assets) {
75
+ this.registerAssets(data.component.latestVersion.assets.filter((asset) => asset.contents));
76
+ }
77
+ }
78
+ }
79
+ export const assetRegistry = new AssetRegistry();
@@ -0,0 +1,78 @@
1
+ import * as fs from "fs/promises";
2
+ import fetch from "node-fetch";
3
+ import * as path from "path";
4
+ import { assetRegistry } from "./asset-registry.js";
5
+ import { createErrorResponse, createResponse } from "./response-utils.js";
6
+ /**
7
+ * Finds an asset by its source ID from the asset registry
8
+ * @param sourceId The source ID to search for
9
+ * @returns Object containing the asset URL and information or undefined
10
+ */
11
+ export function findAssetById(sourceId) {
12
+ const record = assetRegistry.getAssetRecord(sourceId);
13
+ if (!record || record.contents.length === 0)
14
+ return undefined;
15
+ return {
16
+ url: record.contents[0].url,
17
+ format: record.contents[0].format,
18
+ displayName: record.displayName
19
+ };
20
+ }
21
+ /**
22
+ * Gets a downloadable asset URL for the specified format
23
+ * @param sourceId The source ID of the asset
24
+ * @param format The desired file format
25
+ * @returns The URL or undefined if not available
26
+ */
27
+ export function getAssetUrl(sourceId, format) {
28
+ return assetRegistry.getAssetUrl(sourceId, format);
29
+ }
30
+ /**
31
+ * Removes assets from response data to reduce payload size
32
+ * @param data The data to sanitize
33
+ * @returns The data with assets removed
34
+ */
35
+ export function sanitizeResponse(data) {
36
+ if (!data)
37
+ return data;
38
+ assetRegistry.extractAssetsFromData(data);
39
+ const result = JSON.parse(JSON.stringify(data));
40
+ if (Array.isArray(result.variants)) {
41
+ result.variants.forEach((variant) => {
42
+ delete variant.assets;
43
+ });
44
+ }
45
+ if (result.component?.latestVersion) {
46
+ delete result.component.latestVersion.assets;
47
+ }
48
+ return result;
49
+ }
50
+ /**
51
+ * Downloads an asset from a URL and saves it to the specified local directory
52
+ * @param assetUrl The URL of the asset to download
53
+ * @param localDir The local directory path to save the asset
54
+ * @returns Response with download status or error message
55
+ */
56
+ export async function downloadAsset(assetUrl, localDir) {
57
+ try {
58
+ const url = new URL(assetUrl);
59
+ const urlPath = url.pathname;
60
+ const fileExtension = path.extname(urlPath) || ".png";
61
+ const assetId = path.basename(urlPath, fileExtension);
62
+ await fs.mkdir(localDir, { recursive: true });
63
+ const fileName = assetId + fileExtension;
64
+ const filePath = path.join(localDir, fileName);
65
+ const response = await fetch(assetUrl);
66
+ if (!response.ok) {
67
+ return createErrorResponse(`Failed to download asset: Server responded with ${response.status}`);
68
+ }
69
+ const buffer = await response.arrayBuffer();
70
+ await fs.writeFile(filePath, Buffer.from(buffer));
71
+ return createResponse({
72
+ message: `Asset successfully downloaded to ${filePath}`
73
+ });
74
+ }
75
+ catch (error) {
76
+ return createErrorResponse(`Failed to download asset: ${error instanceof Error ? error.message : String(error)}`);
77
+ }
78
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Central export file for all utilities
3
+ */
4
+ // Asset utilities
5
+ export * from "./asset-utils.js";
6
+ // URL utilities
7
+ export * from "./url-utils.js";
8
+ // API utilities
9
+ export * from "./api-utils.js";
10
+ // Preprocessing utilities
11
+ export * from "./preprocessing.js";
12
+ // Response utilities
13
+ export * from "./response-utils.js";
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Preprocess a screen object to remove unnecessary information
3
+ * @param screen The screen object to preprocess
4
+ * @returns The processed screen object
5
+ */
6
+ export function preProcessScreen(screen) {
7
+ return preProcess(screen);
8
+ }
9
+ /**
10
+ * Preprocess a design tokens object to remove metadata
11
+ * @param designTokens The design tokens to preprocess
12
+ * @returns The processed design tokens
13
+ */
14
+ export function preProcessDesignTokens(designTokens) {
15
+ const processedTokens = preProcess(designTokens);
16
+ return removeMetadata(processedTokens);
17
+ }
18
+ /**
19
+ * Generic preprocessing function that removes unnecessary properties from objects
20
+ * @param data The data to preprocess
21
+ * @returns Processed data with unnecessary properties removed
22
+ */
23
+ export function preProcess(data) {
24
+ if (Array.isArray(data)) {
25
+ return data
26
+ .map(item => preProcess(item))
27
+ .filter(item => {
28
+ if (Array.isArray(item) && item.length === 0)
29
+ return false;
30
+ return true;
31
+ });
32
+ }
33
+ else if (data !== null && typeof data === "object") {
34
+ const result = {};
35
+ // Skip unnecessary properties
36
+ const propsToSkip = new Set(["created", "creator", "thumbnails", "id"]);
37
+ const defaultValues = new Map([
38
+ ["opacity", 1],
39
+ ["blend_mode", "normal"],
40
+ ["rotation", 0]
41
+ ]);
42
+ for (const [key, value] of Object.entries(data)) {
43
+ if (propsToSkip.has(key))
44
+ continue;
45
+ if (defaultValues.has(key) && defaultValues.get(key) === value)
46
+ continue;
47
+ let processedValue;
48
+ if (key === "contents" && Array.isArray(value)) {
49
+ const itemsToKeep = value.filter(originalContentItem => {
50
+ if (originalContentItem && typeof originalContentItem === "object" && originalContentItem !== null && "density" in originalContentItem) {
51
+ return originalContentItem.density === 1;
52
+ }
53
+ if (Array.isArray(originalContentItem) && originalContentItem.length === 0) {
54
+ return false;
55
+ }
56
+ return true;
57
+ });
58
+ processedValue = itemsToKeep.map(keptItem => preProcess(keptItem))
59
+ .filter(processedItem => {
60
+ if (Array.isArray(processedItem) && processedItem.length === 0) {
61
+ return false;
62
+ }
63
+ return true;
64
+ });
65
+ }
66
+ else {
67
+ processedValue = preProcess(value);
68
+ }
69
+ if (Array.isArray(processedValue) && processedValue.length === 0) {
70
+ continue;
71
+ }
72
+ result[key] = processedValue;
73
+ }
74
+ return result;
75
+ }
76
+ else {
77
+ return data;
78
+ }
79
+ }
80
+ /**
81
+ * Removes metadata from design tokens
82
+ * @param obj Object containing design tokens
83
+ * @returns Object with metadata removed
84
+ */
85
+ function removeMetadata(obj) {
86
+ if (Array.isArray(obj)) {
87
+ return obj.map(removeMetadata);
88
+ }
89
+ else if (obj !== null && typeof obj === "object") {
90
+ const result = {};
91
+ for (const [key, value] of Object.entries(obj)) {
92
+ if (key === "metadata")
93
+ continue;
94
+ result[key] = removeMetadata(value);
95
+ }
96
+ return result;
97
+ }
98
+ else {
99
+ return obj;
100
+ }
101
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Creates an error response with consistent formatting
3
+ * @param message Error message to display
4
+ * @returns Formatted error response
5
+ */
6
+ export function createErrorResponse(message) {
7
+ return {
8
+ content: [
9
+ {
10
+ type: "text",
11
+ text: message,
12
+ },
13
+ ],
14
+ isError: true,
15
+ };
16
+ }
17
+ /**
18
+ * Creates a success response with consistent formatting
19
+ * @param data The data to include in the response
20
+ * @param instructionsTemplate The template for instructions to include
21
+ * @returns Formatted success response
22
+ */
23
+ export function createSuccessResponse(data, instructionsTemplate) {
24
+ return {
25
+ content: [
26
+ {
27
+ type: "text",
28
+ text: `${instructionsTemplate}
29
+
30
+ ${typeof data === "string" ? data : `${data.type || ""} data in JSON format:
31
+ ${JSON.stringify(data, null, 2)}`}`,
32
+ },
33
+ ],
34
+ };
35
+ }
36
+ /**
37
+ * Creates a generic response based on provided options
38
+ * @param options Response options including message, error status, and data
39
+ * @returns Formatted API response
40
+ */
41
+ export function createResponse(options) {
42
+ const content = [];
43
+ if (options.message) {
44
+ content.push({
45
+ type: "text",
46
+ text: options.message
47
+ });
48
+ }
49
+ if (options.data && typeof options.data !== "string" && !options.isError) {
50
+ content.push({
51
+ type: "text",
52
+ text: JSON.stringify(options.data, null, 2)
53
+ });
54
+ }
55
+ return {
56
+ content,
57
+ isError: options.isError
58
+ };
59
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * URL patterns for different Zeplin resource types
3
+ */
4
+ export const URL_PATTERNS = {
5
+ COMPONENT: /^https:\/\/app\.zeplin\.io\/styleguide\/([^/]+)\/component\/([^/]+)/,
6
+ PROJECT_STYLEGUIDE_COMPONENT: /^https:\/\/app\.zeplin\.io\/project\/([^/]+)\/styleguide\/component\/([^/]+)/,
7
+ SCREEN: /^https:\/\/app\.zeplin\.io\/project\/([^/]+)\/screen\/([^/]+)/
8
+ };
9
+ /**
10
+ * Resolves a Zeplin shortlink to its full URL
11
+ * @param url The URL or shortlink to resolve
12
+ * @returns The resolved URL
13
+ */
14
+ export async function resolveUrl(url) {
15
+ if (!url.startsWith("https://zpl.io/")) {
16
+ return url;
17
+ }
18
+ try {
19
+ const response = await fetch(url, {
20
+ method: "GET",
21
+ headers: {
22
+ "Accept": "application/json"
23
+ },
24
+ });
25
+ if (!response.ok) {
26
+ throw new Error(`Failed to resolve URL: ${response.status} ${response.statusText}`);
27
+ }
28
+ const data = await response.json();
29
+ return data.url;
30
+ }
31
+ catch (error) {
32
+ throw new Error(`Failed to resolve URL: ${error instanceof Error ? error.message : String(error)}`);
33
+ }
34
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@zeplin/mcp-server",
3
+ "version": "0.2.0",
4
+ "description": "An MCP server for Zeplin for AI assisted UI development.",
5
+ "author": "Zeplin",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/zeplin/mcp-server.git"
10
+ },
11
+ "homepage": "https://github.com/zeplin/mcp-server#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/zeplin/mcp-server/issues"
14
+ },
15
+ "keywords": [
16
+ "zeplin",
17
+ "design",
18
+ "mcp",
19
+ "model context protocol",
20
+ "ai"
21
+ ],
22
+ "bin": {
23
+ "zeplin-mcp-server": "./dist/index.js"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "type": "module",
29
+ "scripts": {
30
+ "dev": "tsc -w",
31
+ "build": "tsc",
32
+ "lint": "eslint --ext .ts src",
33
+ "lint:fix": "eslint --ext .ts --fix src",
34
+ "inspect": "source .env && npx @modelcontextprotocol/inspector -e ZEPLIN_ACCESS_TOKEN=$ZEPLIN_ACCESS_TOKEN node dist/index.js",
35
+ "prepublishOnly": "npm run lint && npm run build"
36
+ },
37
+ "dependencies": {
38
+ "@modelcontextprotocol/sdk": "^1.10.2",
39
+ "@zeplin/sdk": "^1.25.0",
40
+ "node-fetch": "^3.3.2",
41
+ "zod": "^3.24.3"
42
+ },
43
+ "devDependencies": {
44
+ "@eslint/js": "^9.27.0",
45
+ "@types/jest": "^29.5.14",
46
+ "@types/node": "^22.14.1",
47
+ "@typescript-eslint/eslint-plugin": "^8.32.1",
48
+ "@typescript-eslint/parser": "^8.32.1",
49
+ "eslint": "^9.27.0",
50
+ "eslint-plugin-import": "^2.31.0",
51
+ "globals": "^16.1.0",
52
+ "typescript": "^5.8.3"
53
+ }
54
+ }