Skip to content
Closed
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
250 changes: 82 additions & 168 deletions app/src/main/kotlin/kr/co/metadata/mcp/mcp/StyleService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,227 +3,141 @@ package kr.co.metadata.mcp.mcp
import io.modelcontextprotocol.kotlin.sdk.*
import io.modelcontextprotocol.kotlin.sdk.server.Server
import kotlinx.serialization.json.*
import kr.co.metadata.mcp.mcp.utils.SchemaBuilders
import kr.co.metadata.mcp.mcp.utils.ToolRegistrationHelper
import kr.co.metadata.mcp.mcp.utils.ParameterExtractors.requireString
import kr.co.metadata.mcp.mcp.utils.ParameterExtractors.requireDouble
import kr.co.metadata.mcp.mcp.utils.ParameterExtractors.optionalDouble
import kr.co.metadata.mcp.mcp.utils.ColorUtils

/**
* Style service for Figma MCP tools
* Handles styling operations for Figma elements
*
* REFACTORED: Reduced from 229 lines to ~130 lines using utility classes
* - Eliminated repetitive JSON schema building
* - Simplified parameter extraction
* - Centralized color parsing logic
*/
object StyleService : BaseFigmaService() {

/**
* Register all style-related tools with the MCP server
*/
fun registerTools(server: Server, figmaCommandSender: suspend (String, Map<String, Any>) -> Any) {
logger.info { "Registering style service tools..." }

registerSetFillColor(server, figmaCommandSender)
registerSetStrokeColor(server, figmaCommandSender)
registerSetCornerRadius(server, figmaCommandSender)

logger.info { "Style service tools registered successfully" }
}

/**
* Set the fill color of a node in Figma
* Refactored: Reduced from 63 lines to 27 lines (-57%)
*/
private fun registerSetFillColor(server: Server, figmaCommandSender: suspend (String, Map<String, Any>) -> Any) {
server.addTool(
ToolRegistrationHelper.registerSimpleTool(
server = server,
name = "set_fill_color",
description = "Set the fill color of a node in Figma can be TextNode or FrameNode",
inputSchema = Tool.Input(
properties = buildJsonObject {
putJsonObject("nodeId") {
put("type", "string")
put("description", "The ID of the node to modify")
}
putJsonObject("r") {
put("type", "number")
put("minimum", 0)
put("maximum", 1)
put("description", "Red component (0-1)")
}
putJsonObject("g") {
put("type", "number")
put("minimum", 0)
put("maximum", 1)
put("description", "Green component (0-1)")
}
putJsonObject("b") {
put("type", "number")
put("minimum", 0)
put("maximum", 1)
put("description", "Blue component (0-1)")
}
putJsonObject("a") {
put("type", "number")
put("minimum", 0)
put("maximum", 1)
put("description", "Alpha component (0-1)")
}
},
inputSchema = SchemaBuilders.buildToolInput(
SchemaBuilders.nodeIdProperty("The ID of the node to modify"),
*SchemaBuilders.colorComponentProperties().toTypedArray(),
required = listOf("nodeId", "r", "g", "b")
)
) { request ->
kotlinx.coroutines.runBlocking {
try {
val nodeId = request.arguments["nodeId"].safeString("nodeId")
val r = request.arguments["r"].safeDouble("r")
val g = request.arguments["g"].safeDouble("g")
val b = request.arguments["b"].safeDouble("b")
val a = request.arguments["a"]?.safeDoubleOrDefault(1.0) ?: 1.0

val params = mapOf(
"nodeId" to nodeId,
"color" to mapOf(
"r" to r,
"g" to g,
"b" to b,
"a" to a
)
)

val result = figmaCommandSender("set_fill_color", params)
createSuccessResponse("Set fill color of node to RGBA($r, $g, $b, $a): ${result}")
} catch (e: Exception) {
createErrorResponse("setting fill color", e)
}
}
val nodeId = request.arguments.requireString("nodeId")
val color = ColorUtils.parseColorComponents(request.arguments)

val params = mapOf(
"nodeId" to nodeId,
"color" to color
)

val result = figmaCommandSender("set_fill_color", params)
createSuccessResponse("Set fill color of node to ${ColorUtils.formatColor(color)}: $result")
}
}

/**
* Set the stroke color of a node in Figma
* Refactored: Reduced from 76 lines to 30 lines (-61%)
*/
private fun registerSetStrokeColor(server: Server, figmaCommandSender: suspend (String, Map<String, Any>) -> Any) {
server.addTool(
ToolRegistrationHelper.registerSimpleTool(
server = server,
name = "set_stroke_color",
description = "Set the stroke color of a node in Figma",
inputSchema = Tool.Input(
properties = buildJsonObject {
putJsonObject("nodeId") {
put("type", "string")
put("description", "The ID of the node to modify")
}
putJsonObject("r") {
put("type", "number")
put("minimum", 0)
put("maximum", 1)
put("description", "Red component (0-1)")
}
putJsonObject("g") {
put("type", "number")
put("minimum", 0)
put("maximum", 1)
put("description", "Green component (0-1)")
}
putJsonObject("b") {
put("type", "number")
put("minimum", 0)
put("maximum", 1)
put("description", "Blue component (0-1)")
}
putJsonObject("a") {
put("type", "number")
put("minimum", 0)
put("maximum", 1)
put("description", "Alpha component (0-1)")
}
putJsonObject("weight") {
put("type", "number")
put("minimum", 0)
put("description", "Stroke weight")
}
},
inputSchema = SchemaBuilders.buildToolInput(
SchemaBuilders.nodeIdProperty("The ID of the node to modify"),
*SchemaBuilders.colorComponentProperties().toTypedArray(),
SchemaBuilders.numberProperty("weight", "Stroke weight", minimum = 0.0),
required = listOf("nodeId", "r", "g", "b")
)
) { request ->
kotlinx.coroutines.runBlocking {
try {
val nodeId = request.arguments["nodeId"].safeString("nodeId")
val r = request.arguments["r"].safeDouble("r")
val g = request.arguments["g"].safeDouble("g")
val b = request.arguments["b"].safeDouble("b")
val a = request.arguments["a"]?.safeDoubleOrDefault(1.0) ?: 1.0
val weight = request.arguments["weight"]?.safeDoubleOrDefault(1.0) ?: 1.0

val params = mapOf(
"nodeId" to nodeId,
"color" to mapOf(
"r" to r,
"g" to g,
"b" to b,
"a" to a
),
"weight" to weight
)

val result = figmaCommandSender("set_stroke_color", params)
createSuccessResponse("Set stroke color of node to RGBA($r, $g, $b, $a) with weight $weight: ${result}")
} catch (e: Exception) {
createErrorResponse("setting stroke color", e)
}
}
val nodeId = request.arguments.requireString("nodeId")
val color = ColorUtils.parseColorComponents(request.arguments)
val weight = request.arguments.optionalDouble("weight", 1.0)!!

val params = mapOf(
"nodeId" to nodeId,
"color" to color,
"weight" to weight
)

val result = figmaCommandSender("set_stroke_color", params)
createSuccessResponse("Set stroke color of node to ${ColorUtils.formatColor(color)} with weight $weight: $result")
}
}

/**
* Set the corner radius of a node in Figma
* Refactored: Reduced from 58 lines to 36 lines (-38%)
*/
private fun registerSetCornerRadius(server: Server, figmaCommandSender: suspend (String, Map<String, Any>) -> Any) {
server.addTool(
ToolRegistrationHelper.registerSimpleTool(
server = server,
name = "set_corner_radius",
description = "Set the corner radius of a node in Figma",
inputSchema = Tool.Input(
properties = buildJsonObject {
putJsonObject("nodeId") {
put("type", "string")
put("description", "The ID of the node to modify")
}
putJsonObject("radius") {
put("type", "number")
put("minimum", 0)
put("description", "Corner radius value")
}
putJsonObject("corners") {
put("type", "array")
put("description", "Optional array of 4 booleans to specify which corners to round [topLeft, topRight, bottomRight, bottomLeft]")
putJsonObject("items") {
put("type", "boolean")
}
put("minItems", 4)
put("maxItems", 4)
inputSchema = SchemaBuilders.buildToolInput(
SchemaBuilders.nodeIdProperty("The ID of the node to modify"),
SchemaBuilders.numberProperty("radius", "Corner radius value", minimum = 0.0),
"corners" to buildJsonObject {
put("type", "array")
put("description", "Optional array of 4 booleans to specify which corners to round [topLeft, topRight, bottomRight, bottomLeft]")
putJsonObject("items") {
put("type", "boolean")
}
put("minItems", 4)
put("maxItems", 4)
},
required = listOf("nodeId", "radius")
)
) { request ->
kotlinx.coroutines.runBlocking {
try {
val nodeId = request.arguments["nodeId"].safeString("nodeId")
val radius = request.arguments["radius"].safeDouble("radius")

val params = mutableMapOf<String, Any>(
"nodeId" to nodeId,
"radius" to radius
)

// Handle corners array if provided (only include if actually provided)
request.arguments["corners"]?.jsonArray?.let { cornersArray ->
if (cornersArray.size == 4) {
val corners = cornersArray.map { it.jsonPrimitive.boolean }
params["corners"] = corners
logger.debug { "corners array provided: $corners" }
} else {
logger.warn { "corners array must have exactly 4 elements, got ${cornersArray.size}. Ignoring." }
}
}

val result = figmaCommandSender("set_corner_radius", params)
createSuccessResponse("Set corner radius of node to ${radius}px: ${result}")
} catch (e: Exception) {
createErrorResponse("setting corner radius", e)
val nodeId = request.arguments.requireString("nodeId")
val radius = request.arguments.requireDouble("radius")

val params = mutableMapOf<String, Any>(
"nodeId" to nodeId,
"radius" to radius
)

// Handle corners array if provided
request.arguments["corners"]?.jsonArray?.let { cornersArray ->
if (cornersArray.size == 4) {
val corners = cornersArray.map { it.jsonPrimitive.boolean }
params["corners"] = corners
logger.debug { "corners array provided: $corners" }
} else {
logger.warn { "corners array must have exactly 4 elements, got ${cornersArray.size}. Ignoring." }
}
}

val result = figmaCommandSender("set_corner_radius", params)
createSuccessResponse("Set corner radius of node to ${radius}px: $result")
}
}
}
66 changes: 66 additions & 0 deletions app/src/main/kotlin/kr/co/metadata/mcp/mcp/utils/ColorUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package kr.co.metadata.mcp.mcp.utils

import kotlinx.serialization.json.*

/**
* Utilities for parsing and handling color objects
*/
object ColorUtils {

/**
* Parse an RGBA color object from JSON
* Returns a map with r, g, b, a components
*/
fun parseColorObject(colorJson: JsonObject?): Map<String, Double>? {
if (colorJson == null) return null

val colorMap = mutableMapOf<String, Double>()
colorJson["r"]?.jsonPrimitive?.double?.let { colorMap["r"] = it }
colorJson["g"]?.jsonPrimitive?.double?.let { colorMap["g"] = it }
colorJson["b"]?.jsonPrimitive?.double?.let { colorMap["b"] = it }
colorJson["a"]?.jsonPrimitive?.double?.let { colorMap["a"] = it }

return if (colorMap.isNotEmpty()) colorMap else null
}

/**
* Parse RGBA color components from individual arguments
* Extracts r, g, b, a from request arguments
*/
fun parseColorComponents(
arguments: JsonObject,
alphaDefault: Double = 1.0
): Map<String, Double> {
return mapOf(
"r" to (arguments["r"]?.jsonPrimitive?.double ?: 0.0),
"g" to (arguments["g"]?.jsonPrimitive?.double ?: 0.0),
"b" to (arguments["b"]?.jsonPrimitive?.double ?: 0.0),
"a" to (arguments["a"]?.jsonPrimitive?.double ?: alphaDefault)
)
}

/**
* Create a default black color
*/
fun defaultBlackColor(): Map<String, Double> {
return mapOf("r" to 0.0, "g" to 0.0, "b" to 0.0, "a" to 1.0)
}

/**
* Create a default white color
*/
fun defaultWhiteColor(): Map<String, Double> {
return mapOf("r" to 1.0, "g" to 1.0, "b" to 1.0, "a" to 1.0)
}

/**
* Format a color map as a readable string
*/
fun formatColor(color: Map<String, Double>): String {
val r = color["r"] ?: 0.0
val g = color["g"] ?: 0.0
val b = color["b"] ?: 0.0
val a = color["a"] ?: 1.0
return "RGBA($r, $g, $b, $a)"
}
}
Loading