Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/major-shoes-carry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"figma-developer-mcp": minor
---

Introduce CLI option for downloading and persisting Figma data
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default [
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"@typescript-eslint/no-explicit-any": "warn",
"no-undef": "off",
},
},
{
Expand Down
37 changes: 25 additions & 12 deletions src/bin.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
#!/usr/bin/env node

import { config } from "dotenv";
import { resolve } from "path";
import { startServer } from "./server.js";

// Load .env from the current working directory
config({ path: resolve(process.cwd(), ".env") });

// Start the server immediately - this file is only for execution
startServer().catch((error) => {
console.error("Failed to start server:", error);
process.exit(1);
});
import { startServerConfigurable } from "./server.js";
import { executeOnce } from "./exec-mode.js";
import { getParsedArgs, getServerConfig } from "./config.js";

const argv = getParsedArgs();

// Route to exec mode or server mode
const execUrl = argv.exec || argv.e;

if (execUrl) {
// Exec mode: one-off data fetch, then exit
const config = getServerConfig(argv, false, true);
executeOnce(execUrl, config.auth, config.outputFormat).catch((error) => {
console.error("Failed to execute:", error);
process.exit(1);
});
} else {
// Server mode (stdio or HTTP)
const isStdioMode = process.env.NODE_ENV === "cli" || argv.stdio === true;

startServerConfigurable(argv, isStdioMode).catch((error) => {
console.error("Failed to start server:", error);
process.exit(1);
});
}
48 changes: 33 additions & 15 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import { config as loadEnv } from "dotenv";
import { resolve } from "path";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { resolve } from "path";
import type { FigmaAuthOptions } from "./services/figma.js";

export interface CliArgs {
"figma-api-key"?: string;
"figma-oauth-token"?: string;
env?: string;
port?: number;
json?: boolean;
"skip-image-downloads"?: boolean;
exec?: string;
e?: string;
stdio?: boolean;
}

interface ServerConfig {
auth: FigmaAuthOptions;
port: number;
Expand All @@ -24,18 +36,8 @@ function maskApiKey(key: string): string {
return `****${key.slice(-4)}`;
}

interface CliArgs {
"figma-api-key"?: string;
"figma-oauth-token"?: string;
env?: string;
port?: number;
json?: boolean;
"skip-image-downloads"?: boolean;
}

export function getServerConfig(isStdioMode: boolean): ServerConfig {
// Parse command line arguments
const argv = yargs(hideBin(process.argv))
export function getParsedArgs(): CliArgs {
return yargs(hideBin(process.argv))
.options({
"figma-api-key": {
type: "string",
Expand Down Expand Up @@ -63,11 +65,27 @@ export function getServerConfig(isStdioMode: boolean): ServerConfig {
description: "Do not register the download_figma_images tool (skip image downloads)",
default: false,
},
exec: {
type: "string",
alias: "e",
description: "Execute a single Figma data fetch from URL and exit (non-server mode)",
},
stdio: {
type: "boolean",
description: "Run server in stdio mode (for MCP clients like Cursor)",
default: false,
},
})
.help()
.version(process.env.NPM_PACKAGE_VERSION ?? "unknown")
.parseSync() as CliArgs;
}

export function getServerConfig(
argv: CliArgs,
isStdioMode: boolean,
isExecMode = false,
): ServerConfig {
// Load environment variables ASAP from custom path or default
let envFilePath: string;
let envFileSource: "cli" | "default";
Expand Down Expand Up @@ -158,8 +176,8 @@ export function getServerConfig(isStdioMode: boolean): ServerConfig {
process.exit(1);
}

// Log configuration sources
if (!isStdioMode) {
// Log configuration sources (skip in exec mode for cleaner output)
if (!isStdioMode && !isExecMode) {
console.log("\nConfiguration:");
console.log(`- ENV_FILE: ${envFilePath} (source: ${config.configSources.envFile})`);
if (auth.useOAuth) {
Expand Down
40 changes: 40 additions & 0 deletions src/exec-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { FigmaService } from "./services/figma.js";
import { simplifyRawFigmaObject, allExtractors } from "./extractors/index.js";
import { parseFigmaUrl } from "./utils/url-parser.js";
import type { FigmaAuthOptions } from "./services/figma.js";
import yaml from "js-yaml";

/**
* Execute a single Figma data fetch and output to stdout, then exit.
* This is a non-server mode for one-off data retrieval.
*/
export async function executeOnce(
figmaUrl: string,
authOptions: FigmaAuthOptions,
outputFormat: "yaml" | "json",
): Promise<void> {
const { fileKey, nodeId: rawNodeId } = parseFigmaUrl(figmaUrl);

// Replace - with : in nodeId for API query Figma API expects
const nodeId = rawNodeId?.replace(/-/g, ":");

const figmaService = new FigmaService(authOptions);

const rawApiResponse = nodeId
? await figmaService.getRawNode(fileKey, nodeId, null)
: await figmaService.getRawFile(fileKey, null);

const simplifiedDesign = simplifyRawFigmaObject(rawApiResponse, allExtractors);

const { nodes, globalVars, ...metadata } = simplifiedDesign;
const result = {
metadata,
nodes,
globalVars,
};

const formattedResult =
outputFormat === "json" ? JSON.stringify(result, null, 2) : yaml.dump(result);

console.log(formattedResult);
}
13 changes: 9 additions & 4 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Server } from "http";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Logger } from "./utils/logger.js";
import { createServer } from "./mcp/index.js";
import { getServerConfig } from "./config.js";
import { getParsedArgs, getServerConfig, type CliArgs } from "./config.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

let httpServer: Server | null = null;
Expand All @@ -20,10 +20,15 @@ const transports = {
* Start the MCP server in either stdio or HTTP mode.
*/
export async function startServer(): Promise<void> {
// Check if we're running in stdio mode (e.g., via CLI)
const isStdioMode = process.env.NODE_ENV === "cli" || process.argv.includes("--stdio");
const argv = getParsedArgs();

const config = getServerConfig(isStdioMode);
// Server mode (stdio or HTTP)
const isStdioMode = process.env.NODE_ENV === "cli" || argv.stdio === true;
await startServerConfigurable(argv, isStdioMode);
}

export async function startServerConfigurable(argv: CliArgs, isStdioMode: boolean): Promise<void> {
const config = getServerConfig(argv, isStdioMode, false);

const server = createServer(config.auth, {
isHTTP: !isStdioMode,
Expand Down