Skip to main content
Glama

Spline MCP Server

by aydinfer
hana-tools.js22.4 kB
/** * Hana Canvas Tools for Spline.design * * Tools for creating and manipulating Hana canvas designs and interactions. * Hana is Spline's 2D canvas for creating interactive 2D designs that can * integrate with 3D scenes. */ import { z } from "zod"; import { fetchFromSplineApi, updateSplineObject } from "../../utils/api-client.js"; /** * Register Hana canvas tools * @param {McpServer} server - The MCP server instance */ export function registerHanaTools(server) { server.tool( "createHanaCanvas", { projectId: z.string().min(1).describe("Project ID"), name: z.string().min(1).describe("Canvas name"), width: z.number().positive().default(1920).describe("Canvas width in pixels"), height: z.number().positive().default(1080).describe("Canvas height in pixels"), background: z.string().optional().describe("Background color (hex/rgba)"), devicePreset: z.enum(["none", "iphone", "ipad", "macbook", "desktop", "custom"]).default("none") .describe("Device frame preset"), }, async ({ projectId, name, width, height, background, devicePreset }) => { try { const result = await fetchFromSplineApi(`/projects/${projectId}/hana`, { method: "POST", body: JSON.stringify({ name, width, height, background, devicePreset, }), }); return { content: [{ type: "text", text: `Created Hana canvas "${name}" (Canvas ID: ${result.canvasId}, Scene ID: ${result.sceneId})` }] }; } catch (error) { return { content: [{ type: "text", text: `Error creating Hana canvas: ${error.message}` }], isError: true }; } } ); server.tool( "addHanaElement", { sceneId: z.string().min(1).describe("Scene ID for Hana canvas"), elementType: z.enum([ "rectangle", "ellipse", "text", "image", "input", "button", "slider", "toggle", "embed", "3d", "group", "path" ]).describe("Element type"), position: z.object({ x: z.number(), y: z.number(), }).describe("Position on canvas"), size: z.object({ width: z.number().positive(), height: z.number().positive(), }).optional().describe("Element size (some elements have default sizes)"), style: z.object({ // Common style properties fill: z.string().optional().describe("Fill color (hex/rgba)"), stroke: z.string().optional().describe("Stroke color (hex/rgba)"), strokeWidth: z.number().min(0).optional().describe("Stroke width"), opacity: z.number().min(0).max(1).default(1).describe("Element opacity"), blur: z.number().min(0).optional().describe("Blur amount"), borderRadius: z.number().min(0).optional().describe("Border radius for rectangles"), rotation: z.number().optional().describe("Rotation angle in degrees"), // Text properties text: z.string().optional().describe("Text content for text elements"), fontFamily: z.string().optional().describe("Font family for text"), fontSize: z.number().positive().optional().describe("Font size"), fontWeight: z.enum(["normal", "bold", "100", "200", "300", "400", "500", "600", "700", "800", "900"]) .optional().describe("Font weight"), textAlign: z.enum(["left", "center", "right"]).optional().describe("Text alignment"), lineHeight: z.number().positive().optional().describe("Line height"), // Image properties imageUrl: z.string().url().optional().describe("Image URL for image elements"), objectFit: z.enum(["cover", "contain", "fill", "none"]).optional().describe("Image object fit"), // Button properties buttonText: z.string().optional().describe("Text for button elements"), buttonStyle: z.enum(["default", "primary", "secondary", "outline", "text"]).optional().describe("Button style"), // Input properties placeholder: z.string().optional().describe("Placeholder text for input elements"), inputType: z.enum(["text", "number", "email", "password"]).optional().describe("Input field type"), // 3D embed properties sceneId: z.string().optional().describe("3D scene ID to embed"), // Effects shadow: z.boolean().default(false).optional().describe("Enable drop shadow"), shadowColor: z.string().optional().describe("Shadow color (hex/rgba)"), shadowOffsetX: z.number().optional().describe("Shadow X offset"), shadowOffsetY: z.number().optional().describe("Shadow Y offset"), shadowBlur: z.number().min(0).optional().describe("Shadow blur amount"), // Advanced properties zIndex: z.number().int().optional().describe("Z-index for stacking order"), overflow: z.enum(["visible", "hidden", "scroll", "auto"]).optional().describe("Overflow behavior"), pointerEvents: z.enum(["auto", "none"]).optional().describe("Pointer events handling"), }).optional().describe("Element style properties"), name: z.string().optional().describe("Element name"), }, async ({ sceneId, elementType, position, size, style, name }) => { try { const result = await fetchFromSplineApi(`/scenes/${sceneId}/hana/elements`, { method: "POST", body: JSON.stringify({ elementType, position, size, style, name, }), }); return { content: [{ type: "text", text: `Added ${elementType} element to Hana canvas (Element ID: ${result.elementId})` }] }; } catch (error) { return { content: [{ type: "text", text: `Error adding Hana element: ${error.message}` }], isError: true }; } } ); server.tool( "updateHanaElement", { sceneId: z.string().min(1).describe("Scene ID for Hana canvas"), elementId: z.string().min(1).describe("Element ID"), position: z.object({ x: z.number(), y: z.number(), }).optional().describe("Updated position"), size: z.object({ width: z.number().positive(), height: z.number().positive(), }).optional().describe("Updated size"), style: z.record(z.any()).optional().describe("Updated style properties"), name: z.string().optional().describe("Updated element name"), }, async ({ sceneId, elementId, position, size, style, name }) => { try { await updateSplineObject(sceneId, elementId, { position, size, style, name, }); return { content: [{ type: "text", text: `Updated Hana element ${elementId}` }] }; } catch (error) { return { content: [{ type: "text", text: `Error updating Hana element: ${error.message}` }], isError: true }; } } ); server.tool( "addHanaInteraction", { sceneId: z.string().min(1).describe("Scene ID for Hana canvas"), elementId: z.string().min(1).describe("Element ID to add interaction to"), eventType: z.enum([ "click", "hover", "mouseEnter", "mouseLeave", "mouseDown", "mouseUp", "drag", "dragStart", "dragEnd", "scroll", "swipe", "keyUp", "keyDown", "keyPress", "focus", "blur", "load", "valueChange" ]).describe("Event type"), actions: z.array(z.object({ actionType: z.enum([ "changeStyle", "changePosition", "changeSize", "show", "hide", "navigate", "openUrl", "playAnimation", "toggleClass", "setValue", "increaseValue", "decreaseValue", "showElement", "hideElement", "toggleElement", "playSound", "playVideo", "stopVideo", "pauseVideo" ]).describe("Action type"), targetId: z.string().min(1).optional().describe("Target element ID (if different from source)"), parameters: z.record(z.any()).describe("Action-specific parameters"), delay: z.number().min(0).default(0).describe("Delay before action in seconds"), duration: z.number().min(0).optional().describe("Action duration in seconds (for animations)"), easing: z.string().optional().describe("Easing function for animations"), })).min(1).describe("Actions to trigger"), name: z.string().optional().describe("Interaction name"), }, async ({ sceneId, elementId, eventType, actions, name }) => { try { const result = await fetchFromSplineApi(`/scenes/${sceneId}/hana/elements/${elementId}/interactions`, { method: "POST", body: JSON.stringify({ eventType, actions, name, }), }); return { content: [{ type: "text", text: `Added ${eventType} interaction to element ${elementId} (Interaction ID: ${result.interactionId})` }] }; } catch (error) { return { content: [{ type: "text", text: `Error adding Hana interaction: ${error.message}` }], isError: true }; } } ); server.tool( "addHanaState", { sceneId: z.string().min(1).describe("Scene ID for Hana canvas"), name: z.string().min(1).describe("State name"), elementStates: z.array(z.object({ elementId: z.string().min(1).describe("Element ID"), properties: z.record(z.any()).describe("Property changes for this element in this state"), })).min(1).describe("Element states for this canvas state"), defaultTransition: z.object({ duration: z.number().positive().default(0.3).describe("Transition duration in seconds"), easing: z.string().default("ease").describe("Transition easing function"), delay: z.number().min(0).default(0).describe("Transition delay in seconds"), }).optional().describe("Default transition properties for this state"), }, async ({ sceneId, name, elementStates, defaultTransition }) => { try { const result = await fetchFromSplineApi(`/scenes/${sceneId}/hana/states`, { method: "POST", body: JSON.stringify({ name, elementStates, defaultTransition, }), }); return { content: [{ type: "text", text: `Created Hana state "${name}" (State ID: ${result.stateId})` }] }; } catch (error) { return { content: [{ type: "text", text: `Error creating Hana state: ${error.message}` }], isError: true }; } } ); server.tool( "triggerHanaState", { sceneId: z.string().min(1).describe("Scene ID for Hana canvas"), stateId: z.string().min(1).describe("State ID"), transitionOverrides: z.object({ duration: z.number().positive().optional().describe("Override transition duration"), easing: z.string().optional().describe("Override easing function"), delay: z.number().min(0).optional().describe("Override transition delay"), }).optional().describe("Transition property overrides"), }, async ({ sceneId, stateId, transitionOverrides }) => { try { await fetchFromSplineApi(`/scenes/${sceneId}/hana/states/${stateId}/trigger`, { method: "POST", body: JSON.stringify({ transitionOverrides, }), }); return { content: [{ type: "text", text: `Triggered Hana state ${stateId}` }] }; } catch (error) { return { content: [{ type: "text", text: `Error triggering Hana state: ${error.message}` }], isError: true }; } } ); server.tool( "createHanaAnimation", { sceneId: z.string().min(1).describe("Scene ID for Hana canvas"), name: z.string().min(1).describe("Animation name"), targetElementId: z.string().min(1).describe("Target element ID"), keyframes: z.array(z.object({ time: z.number().min(0).describe("Keyframe time in seconds"), properties: z.record(z.any()).describe("Element properties at this keyframe"), })).min(2).describe("Animation keyframes"), duration: z.number().positive().describe("Animation duration in seconds"), easing: z.string().default("linear").describe("Animation easing function"), loop: z.boolean().default(false).describe("Whether to loop the animation"), autoPlay: z.boolean().default(false).describe("Whether to play the animation automatically"), }, async ({ sceneId, name, targetElementId, keyframes, duration, easing, loop, autoPlay }) => { try { const result = await fetchFromSplineApi(`/scenes/${sceneId}/hana/animations`, { method: "POST", body: JSON.stringify({ name, targetElementId, keyframes, duration, easing, loop, autoPlay, }), }); return { content: [{ type: "text", text: `Created Hana animation "${name}" (Animation ID: ${result.animationId})` }] }; } catch (error) { return { content: [{ type: "text", text: `Error creating Hana animation: ${error.message}` }], isError: true }; } } ); server.tool( "controlHanaAnimation", { sceneId: z.string().min(1).describe("Scene ID for Hana canvas"), animationId: z.string().min(1).describe("Animation ID"), action: z.enum(["play", "pause", "stop", "reverse"]).describe("Animation control action"), playbackRate: z.number().positive().optional().describe("Playback speed (1.0 = normal)"), }, async ({ sceneId, animationId, action, playbackRate }) => { try { await fetchFromSplineApi(`/scenes/${sceneId}/hana/animations/${animationId}/control`, { method: "POST", body: JSON.stringify({ action, playbackRate, }), }); return { content: [{ type: "text", text: `Hana animation ${animationId} ${action} command sent` }] }; } catch (error) { return { content: [{ type: "text", text: `Error controlling Hana animation: ${error.message}` }], isError: true }; } } ); server.tool( "addHanaEffect", { sceneId: z.string().min(1).describe("Scene ID for Hana canvas"), elementId: z.string().min(1).describe("Target element ID"), effectType: z.enum([ "blur", "glow", "neon", "shadow", "gradient", "noise", "grain", "distortion", "wavy", "ripple" ]).describe("Effect type"), parameters: z.object({ // Blur effect parameters blurAmount: z.number().min(0).optional().describe("Blur amount (px)"), // Glow effect parameters glowColor: z.string().optional().describe("Glow color (hex/rgba)"), glowRadius: z.number().min(0).optional().describe("Glow radius (px)"), glowIntensity: z.number().min(0).optional().describe("Glow intensity"), // Neon effect parameters neonColor: z.string().optional().describe("Neon color (hex/rgba)"), neonWidth: z.number().min(0).optional().describe("Neon width (px)"), neonIntensity: z.number().min(0).optional().describe("Neon intensity"), // Shadow effect parameters shadowColor: z.string().optional().describe("Shadow color (hex/rgba)"), shadowOffsetX: z.number().optional().describe("Shadow X offset (px)"), shadowOffsetY: z.number().optional().describe("Shadow Y offset (px)"), shadowBlur: z.number().min(0).optional().describe("Shadow blur (px)"), // Gradient effect parameters gradientType: z.enum(["linear", "radial", "conic"]).optional().describe("Gradient type"), gradientColors: z.array(z.string()).optional().describe("Gradient colors (hex/rgba)"), gradientStops: z.array(z.number().min(0).max(1)).optional().describe("Gradient color stops (0-1)"), gradientAngle: z.number().optional().describe("Gradient angle (degrees, for linear)"), // Noise/grain effect parameters noiseType: z.enum(["perlin", "simplex", "value", "fractal"]).optional().describe("Noise type"), noiseScale: z.number().positive().optional().describe("Noise scale"), noiseAmount: z.number().min(0).max(1).optional().describe("Noise amount"), // Distortion effect parameters distortionType: z.enum(["bulge", "pinch", "twist", "wave"]).optional().describe("Distortion type"), distortionAmount: z.number().optional().describe("Distortion amount"), distortionCenter: z.object({ x: z.number().min(0).max(1).optional().describe("Center X (0-1)"), y: z.number().min(0).max(1).optional().describe("Center Y (0-1)"), }).optional().describe("Distortion center point"), // Common parameters blend: z.enum(["normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn"]) .default("normal").optional().describe("Blend mode"), opacity: z.number().min(0).max(1).default(1).optional().describe("Effect opacity"), animate: z.boolean().default(false).optional().describe("Animate the effect"), animationSpeed: z.number().positive().default(1).optional().describe("Animation speed"), }).describe("Effect-specific parameters"), }, async ({ sceneId, elementId, effectType, parameters }) => { try { const result = await fetchFromSplineApi(`/scenes/${sceneId}/hana/elements/${elementId}/effects`, { method: "POST", body: JSON.stringify({ effectType, parameters, }), }); return { content: [{ type: "text", text: `Added ${effectType} effect to element ${elementId} (Effect ID: ${result.effectId})` }] }; } catch (error) { return { content: [{ type: "text", text: `Error adding Hana effect: ${error.message}` }], isError: true }; } } ); server.tool( "createHanaPrototype", { sceneId: z.string().min(1).describe("Scene ID for Hana canvas"), name: z.string().min(1).describe("Prototype name"), startFrame: z.string().min(1).describe("Starting frame/element ID"), description: z.string().optional().describe("Prototype description"), previewImage: z.string().url().optional().describe("Preview image URL"), settings: z.object({ defaultTransition: z.object({ type: z.enum(["none", "fade", "slide", "push", "dissolve"]).default("fade") .describe("Default transition type"), duration: z.number().positive().default(0.3).describe("Default transition duration"), easing: z.string().default("ease").describe("Default transition easing"), }).optional().describe("Default transition settings"), hotspotHinting: z.boolean().default(true).optional().describe("Show hotspot hints"), deviceFrame: z.enum(["none", "iphone", "ipad", "macbook", "desktop", "custom"]).default("none") .optional().describe("Device frame for preview"), }).optional().describe("Prototype settings"), }, async ({ sceneId, name, startFrame, description, previewImage, settings }) => { try { const result = await fetchFromSplineApi(`/scenes/${sceneId}/hana/prototypes`, { method: "POST", body: JSON.stringify({ name, startFrame, description, previewImage, settings, }), }); return { content: [{ type: "text", text: `Created Hana prototype "${name}" (Prototype ID: ${result.prototypeId})` }] }; } catch (error) { return { content: [{ type: "text", text: `Error creating Hana prototype: ${error.message}` }], isError: true }; } } ); server.tool( "exportHanaCanvas", { sceneId: z.string().min(1).describe("Scene ID for Hana canvas"), format: z.enum(["html", "react", "vue", "pdf", "image"]).describe("Export format"), includeAssets: z.boolean().default(true).describe("Include all assets in export"), includeInteractions: z.boolean().default(true).describe("Include interactions in export"), include3D: z.boolean().default(true).describe("Include 3D elements in export"), optimizeFor: z.enum(["web", "mobile", "desktop"]).default("web").describe("Platform optimization"), scale: z.number().positive().default(1).describe("Scale factor for image exports"), }, async ({ sceneId, format, includeAssets, includeInteractions, include3D, optimizeFor, scale }) => { try { const result = await fetchFromSplineApi(`/scenes/${sceneId}/hana/export`, { method: "POST", body: JSON.stringify({ format, includeAssets, includeInteractions, include3D, optimizeFor, scale, }), }); return { content: [{ type: "text", text: `Exported Hana canvas in ${format} format. Download URL: ${result.downloadUrl}` }] }; } catch (error) { return { content: [{ type: "text", text: `Error exporting Hana canvas: ${error.message}` }], isError: true }; } } ); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aydinfer/spline-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server