@rafaylabs/animation-mcp 0.0.0-stage → 1.0.1

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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -2
  3. package/bin/cli.js +50 -0
  4. package/dist/api/catalog-client.d.ts +35 -0
  5. package/dist/api/catalog-client.js +78 -0
  6. package/dist/api/server.d.ts +29 -0
  7. package/dist/api/server.js +55 -0
  8. package/dist/auth/cli-login.d.ts +3 -0
  9. package/dist/auth/cli-login.js +121 -0
  10. package/dist/auth/client.d.ts +45 -0
  11. package/dist/auth/client.js +170 -0
  12. package/dist/auth/config.d.ts +36 -0
  13. package/dist/auth/config.js +106 -0
  14. package/dist/auth/device-login.d.ts +2 -0
  15. package/dist/auth/device-login.js +63 -0
  16. package/dist/auth/product.d.ts +17 -0
  17. package/dist/auth/product.js +9 -0
  18. package/dist/index.d.ts +4 -0
  19. package/dist/index.js +267 -0
  20. package/dist/theming/classifier.d.ts +16 -0
  21. package/dist/theming/classifier.js +74 -0
  22. package/dist/theming/color-walker.d.ts +19 -0
  23. package/dist/theming/color-walker.js +303 -0
  24. package/dist/theming/engine.d.ts +29 -0
  25. package/dist/theming/engine.js +144 -0
  26. package/dist/theming/oklch.d.ts +63 -0
  27. package/dist/theming/oklch.js +147 -0
  28. package/dist/tools/code.d.ts +20 -0
  29. package/dist/tools/code.js +104 -0
  30. package/dist/tools/get.d.ts +13 -0
  31. package/dist/tools/get.js +10 -0
  32. package/dist/tools/save.d.ts +22 -0
  33. package/dist/tools/save.js +78 -0
  34. package/dist/tools/schemas.d.ts +140 -0
  35. package/dist/tools/schemas.js +51 -0
  36. package/dist/tools/search.d.ts +35 -0
  37. package/dist/tools/search.js +30 -0
  38. package/dist/tools/theme.d.ts +28 -0
  39. package/dist/tools/theme.js +66 -0
  40. package/dist/utils/lottie-file.d.ts +5 -0
  41. package/dist/utils/lottie-file.js +47 -0
  42. package/dist/utils/workspace.d.ts +20 -0
  43. package/dist/utils/workspace.js +116 -0
  44. package/package.json +56 -4
@@ -0,0 +1,140 @@
1
+ import { z } from 'zod';
2
+ export declare const SearchAnimationsArgsSchema: z.ZodObject<{
3
+ query: z.ZodString;
4
+ style: z.ZodOptional<z.ZodString>;
5
+ category: z.ZodOptional<z.ZodString>;
6
+ loop: z.ZodOptional<z.ZodBoolean>;
7
+ max_size_kb: z.ZodOptional<z.ZodNumber>;
8
+ limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
9
+ page: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
10
+ }, "strip", z.ZodTypeAny, {
11
+ limit: number;
12
+ page: number;
13
+ query: string;
14
+ category?: string | undefined;
15
+ style?: string | undefined;
16
+ loop?: boolean | undefined;
17
+ max_size_kb?: number | undefined;
18
+ }, {
19
+ query: string;
20
+ limit?: number | undefined;
21
+ page?: number | undefined;
22
+ category?: string | undefined;
23
+ style?: string | undefined;
24
+ loop?: boolean | undefined;
25
+ max_size_kb?: number | undefined;
26
+ }>;
27
+ export declare const GetAnimationArgsSchema: z.ZodObject<{
28
+ id: z.ZodString;
29
+ }, "strip", z.ZodTypeAny, {
30
+ id: string;
31
+ }, {
32
+ id: string;
33
+ }>;
34
+ export declare const ApplyThemeArgsSchema: z.ZodObject<{
35
+ id: z.ZodOptional<z.ZodString>;
36
+ file_path: z.ZodOptional<z.ZodString>;
37
+ lottie_json: z.ZodOptional<z.ZodUnion<[z.ZodRecord<z.ZodString, z.ZodAny>, z.ZodString]>>;
38
+ output_path: z.ZodOptional<z.ZodString>;
39
+ format: z.ZodOptional<z.ZodEnum<["json", "lottie"]>>;
40
+ overwrite: z.ZodOptional<z.ZodBoolean>;
41
+ theme: z.ZodObject<{
42
+ primary: z.ZodString;
43
+ secondary: z.ZodOptional<z.ZodString>;
44
+ background: z.ZodOptional<z.ZodString>;
45
+ accent: z.ZodOptional<z.ZodString>;
46
+ }, "strip", z.ZodTypeAny, {
47
+ primary: string;
48
+ secondary?: string | undefined;
49
+ background?: string | undefined;
50
+ accent?: string | undefined;
51
+ }, {
52
+ primary: string;
53
+ secondary?: string | undefined;
54
+ background?: string | undefined;
55
+ accent?: string | undefined;
56
+ }>;
57
+ mode: z.ZodOptional<z.ZodEnum<["light", "dark"]>>;
58
+ }, "strip", z.ZodTypeAny, {
59
+ theme: {
60
+ primary: string;
61
+ secondary?: string | undefined;
62
+ background?: string | undefined;
63
+ accent?: string | undefined;
64
+ };
65
+ mode?: "light" | "dark" | undefined;
66
+ id?: string | undefined;
67
+ lottie_json?: string | Record<string, any> | undefined;
68
+ format?: "json" | "lottie" | undefined;
69
+ file_path?: string | undefined;
70
+ output_path?: string | undefined;
71
+ overwrite?: boolean | undefined;
72
+ }, {
73
+ theme: {
74
+ primary: string;
75
+ secondary?: string | undefined;
76
+ background?: string | undefined;
77
+ accent?: string | undefined;
78
+ };
79
+ mode?: "light" | "dark" | undefined;
80
+ id?: string | undefined;
81
+ lottie_json?: string | Record<string, any> | undefined;
82
+ format?: "json" | "lottie" | undefined;
83
+ file_path?: string | undefined;
84
+ output_path?: string | undefined;
85
+ overwrite?: boolean | undefined;
86
+ }>;
87
+ export declare const SaveAnimationArgsSchema: z.ZodObject<{
88
+ themed_json: z.ZodUnion<[z.ZodRecord<z.ZodString, z.ZodAny>, z.ZodString]>;
89
+ path: z.ZodString;
90
+ format: z.ZodOptional<z.ZodEnum<["json", "lottie"]>>;
91
+ overwrite: z.ZodOptional<z.ZodBoolean>;
92
+ }, "strip", z.ZodTypeAny, {
93
+ path: string;
94
+ themed_json: string | Record<string, any>;
95
+ format?: "json" | "lottie" | undefined;
96
+ overwrite?: boolean | undefined;
97
+ }, {
98
+ path: string;
99
+ themed_json: string | Record<string, any>;
100
+ format?: "json" | "lottie" | undefined;
101
+ overwrite?: boolean | undefined;
102
+ }>;
103
+ export declare const GetCodeArgsSchema: z.ZodObject<{
104
+ framework: z.ZodEnum<["react", "react-native", "nextjs", "html"]>;
105
+ file_path: z.ZodString;
106
+ options: z.ZodOptional<z.ZodObject<{
107
+ loop: z.ZodOptional<z.ZodBoolean>;
108
+ autoplay: z.ZodOptional<z.ZodBoolean>;
109
+ size: z.ZodOptional<z.ZodNumber>;
110
+ componentName: z.ZodOptional<z.ZodString>;
111
+ }, "strip", z.ZodTypeAny, {
112
+ loop?: boolean | undefined;
113
+ autoplay?: boolean | undefined;
114
+ size?: number | undefined;
115
+ componentName?: string | undefined;
116
+ }, {
117
+ loop?: boolean | undefined;
118
+ autoplay?: boolean | undefined;
119
+ size?: number | undefined;
120
+ componentName?: string | undefined;
121
+ }>>;
122
+ }, "strip", z.ZodTypeAny, {
123
+ file_path: string;
124
+ framework: "react" | "react-native" | "nextjs" | "html";
125
+ options?: {
126
+ loop?: boolean | undefined;
127
+ autoplay?: boolean | undefined;
128
+ size?: number | undefined;
129
+ componentName?: string | undefined;
130
+ } | undefined;
131
+ }, {
132
+ file_path: string;
133
+ framework: "react" | "react-native" | "nextjs" | "html";
134
+ options?: {
135
+ loop?: boolean | undefined;
136
+ autoplay?: boolean | undefined;
137
+ size?: number | undefined;
138
+ componentName?: string | undefined;
139
+ } | undefined;
140
+ }>;
@@ -0,0 +1,51 @@
1
+ import { z } from 'zod';
2
+ export const SearchAnimationsArgsSchema = z.object({
3
+ query: z.string().min(1, 'Search query cannot be empty'),
4
+ style: z.string().optional(),
5
+ category: z.string().optional(),
6
+ loop: z.boolean().optional(),
7
+ max_size_kb: z.number().positive().optional(),
8
+ limit: z.number().int().min(1).max(20).optional().default(5),
9
+ page: z.number().int().min(1).max(50).optional().default(1),
10
+ });
11
+ export const GetAnimationArgsSchema = z.object({
12
+ id: z.string().regex(/^[A-Za-z0-9_-]{1,128}$/, 'Animation ID may only contain letters, numbers, - and _'),
13
+ });
14
+ const HexColorRegex = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
15
+ export const ApplyThemeArgsSchema = z.object({
16
+ id: z.string().regex(/^[A-Za-z0-9_-]{1,128}$/, 'Animation ID may only contain letters, numbers, - and _').optional(),
17
+ file_path: z.string().optional(),
18
+ lottie_json: z.union([z.record(z.any()), z.string()]).optional(),
19
+ output_path: z.string().optional(),
20
+ format: z.enum(['json', 'lottie']).optional(),
21
+ overwrite: z.boolean().optional(),
22
+ theme: z.object({
23
+ primary: z.string().regex(HexColorRegex, 'Primary color must be a valid hex string (e.g. #0476FF)'),
24
+ secondary: z.string().regex(HexColorRegex, 'Secondary color must be a valid hex string').optional(),
25
+ background: z.string().regex(HexColorRegex, 'Background color must be a valid hex string').optional(),
26
+ accent: z.string().regex(HexColorRegex, 'Accent color must be a valid hex string').optional(),
27
+ }),
28
+ mode: z.enum(['light', 'dark']).optional(),
29
+ });
30
+ export const SaveAnimationArgsSchema = z.object({
31
+ themed_json: z.union([z.record(z.any()), z.string()], {
32
+ required_error: 'themed_json is required',
33
+ }),
34
+ path: z.string().min(1, 'Target file path is required'),
35
+ format: z.enum(['json', 'lottie']).optional(),
36
+ overwrite: z.boolean().optional(),
37
+ });
38
+ export const GetCodeArgsSchema = z.object({
39
+ framework: z.enum(['react', 'react-native', 'nextjs', 'html'], {
40
+ errorMap: () => ({ message: 'Framework must be one of: react, react-native, nextjs, html' }),
41
+ }),
42
+ file_path: z.string().min(1, 'file_path is required'),
43
+ options: z
44
+ .object({
45
+ loop: z.boolean().optional(),
46
+ autoplay: z.boolean().optional(),
47
+ size: z.number().positive().optional(),
48
+ componentName: z.string().optional(),
49
+ })
50
+ .optional(),
51
+ });
@@ -0,0 +1,35 @@
1
+ import { z } from 'zod';
2
+ import { CatalogSearchResultItem } from '../api/catalog-client.js';
3
+ export declare const SearchAnimationsSchema: z.ZodObject<{
4
+ query: z.ZodString;
5
+ style: z.ZodOptional<z.ZodString>;
6
+ category: z.ZodOptional<z.ZodString>;
7
+ loop: z.ZodOptional<z.ZodBoolean>;
8
+ max_size_kb: z.ZodOptional<z.ZodNumber>;
9
+ limit: z.ZodDefault<z.ZodNumber>;
10
+ page: z.ZodDefault<z.ZodNumber>;
11
+ }, "strip", z.ZodTypeAny, {
12
+ limit: number;
13
+ page: number;
14
+ query: string;
15
+ category?: string | undefined;
16
+ style?: string | undefined;
17
+ loop?: boolean | undefined;
18
+ max_size_kb?: number | undefined;
19
+ }, {
20
+ query: string;
21
+ limit?: number | undefined;
22
+ page?: number | undefined;
23
+ category?: string | undefined;
24
+ style?: string | undefined;
25
+ loop?: boolean | undefined;
26
+ max_size_kb?: number | undefined;
27
+ }>;
28
+ export type SearchAnimationsInput = z.infer<typeof SearchAnimationsSchema>;
29
+ export declare function handleSearchAnimations(input: SearchAnimationsInput): Promise<{
30
+ results: CatalogSearchResultItem[];
31
+ totalMatches: number;
32
+ page: number;
33
+ totalPages: number;
34
+ has_more: boolean;
35
+ }>;
@@ -0,0 +1,30 @@
1
+ import { z } from 'zod';
2
+ import { searchCatalog } from '../api/catalog-client.js';
3
+ export const SearchAnimationsSchema = z.object({
4
+ query: z.string().min(1, 'Please provide search keywords, e.g. "checkmark", "loading spinner", or "successful payment"'),
5
+ style: z.string().optional().describe('Visual style: flat, 3d, minimal, gradient, outline'),
6
+ category: z.string().optional().describe('Animation category: success, loading, error, empty state, onboarding, ecommerce'),
7
+ loop: z.boolean().optional().describe('Whether the animation should continuously loop'),
8
+ max_size_kb: z.number().optional().describe('Maximum allowed file size in KB'),
9
+ limit: z.number().min(1).max(20).default(5).describe('Results per page, 1-20 (default 5)'),
10
+ page: z.number().int().min(1).max(50).default(1).describe('Page of results (default 1). Ask for the next page when has_more is true and nothing fits yet'),
11
+ });
12
+ export async function handleSearchAnimations(input) {
13
+ // Sign-in, plan and daily limits are enforced by the server; it answers 401/429 with a clear message.
14
+ const found = await searchCatalog({
15
+ query: input.query,
16
+ category: input.category,
17
+ style: input.style,
18
+ loop: input.loop,
19
+ max_size_kb: input.max_size_kb,
20
+ limit: input.limit,
21
+ page: input.page,
22
+ });
23
+ return {
24
+ results: found.results,
25
+ totalMatches: found.total,
26
+ page: found.page,
27
+ totalPages: found.totalPages,
28
+ has_more: found.hasMore,
29
+ };
30
+ }
@@ -0,0 +1,28 @@
1
+ import { UserTheme, ThemingResult } from '../theming/engine.js';
2
+ export interface ApplyThemeInput {
3
+ id?: string;
4
+ lottie_json?: Record<string, any> | string;
5
+ file_path?: string;
6
+ output_path?: string;
7
+ format?: 'json' | 'lottie';
8
+ overwrite?: boolean;
9
+ theme: UserTheme;
10
+ mode?: 'light' | 'dark';
11
+ }
12
+ export interface ApplyThemeSummaryResult {
13
+ success: boolean;
14
+ colorMap: ThemingResult['colorMap'];
15
+ themeableScore: number;
16
+ totalColorsCount: number;
17
+ hasRasterImages: boolean;
18
+ savedPath?: string;
19
+ absolutePath?: string;
20
+ sizeBytes?: number;
21
+ sizeKb?: string;
22
+ format?: 'json' | 'lottie';
23
+ note?: string;
24
+ }
25
+ /**
26
+ * Handles apply_theme tool execution
27
+ */
28
+ export declare function executeApplyTheme(input: ApplyThemeInput, server?: any): Promise<ApplyThemeSummaryResult>;
@@ -0,0 +1,66 @@
1
+ import fs from 'fs';
2
+ import { applyThemeToLottie } from '../theming/engine.js';
3
+ import { fetchAnimationById } from '../api/catalog-client.js';
4
+ import { resolveSafePath } from '../utils/workspace.js';
5
+ import { saveAnimationToProject } from './save.js';
6
+ import { readLottieFile } from '../utils/lottie-file.js';
7
+ /**
8
+ * Handles apply_theme tool execution
9
+ */
10
+ export async function executeApplyTheme(input, server) {
11
+ let lottieData = null;
12
+ // 1. From local file in user's workspace
13
+ if (input.file_path) {
14
+ const { absolutePath } = await resolveSafePath(input.file_path, { server });
15
+ if (!fs.existsSync(absolutePath)) {
16
+ throw new Error(`Animation file not found at path: ${input.file_path}`);
17
+ }
18
+ lottieData = readLottieFile(absolutePath);
19
+ }
20
+ // 2. From direct JSON input
21
+ if (!lottieData && input.lottie_json) {
22
+ lottieData =
23
+ typeof input.lottie_json === 'string'
24
+ ? JSON.parse(input.lottie_json)
25
+ : input.lottie_json;
26
+ }
27
+ // 3. From hosted catalogue ID
28
+ if (!lottieData && input.id) {
29
+ lottieData = await fetchAnimationById(input.id);
30
+ }
31
+ if (!lottieData) {
32
+ throw new Error('Please provide either `file_path`, `lottie_json`, or a catalogue `id`.');
33
+ }
34
+ // Apply theme
35
+ const themingResult = applyThemeToLottie(lottieData, input.theme, { mode: input.mode });
36
+ // If output_path is provided, save directly to project workspace
37
+ if (input.output_path) {
38
+ const saveResult = await saveAnimationToProject({
39
+ themed_json: themingResult.themedLottie,
40
+ path: input.output_path,
41
+ format: input.format,
42
+ overwrite: input.overwrite,
43
+ }, server);
44
+ return {
45
+ success: true,
46
+ colorMap: themingResult.colorMap,
47
+ themeableScore: themingResult.themeableScore,
48
+ totalColorsCount: themingResult.totalColorsCount,
49
+ hasRasterImages: themingResult.hasRasterImages,
50
+ savedPath: saveResult.savedPath,
51
+ absolutePath: saveResult.absolutePath,
52
+ sizeBytes: saveResult.sizeBytes,
53
+ sizeKb: saveResult.sizeKb,
54
+ format: saveResult.format,
55
+ };
56
+ }
57
+ // If output_path is omitted
58
+ return {
59
+ success: true,
60
+ colorMap: themingResult.colorMap,
61
+ themeableScore: themingResult.themeableScore,
62
+ totalColorsCount: themingResult.totalColorsCount,
63
+ hasRasterImages: themingResult.hasRasterImages,
64
+ note: 'Theming succeeded! Specify "output_path" (e.g. "assets/hero.json" or "assets/hero.lottie") to write directly to your project without filling AI context.',
65
+ };
66
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Reads a local animation file. Accepts plain Lottie JSON and dotLottie (.lottie)
3
+ * containers (v1 `animations/<id>.json` and v2 `a/<id>.json` layouts).
4
+ */
5
+ export declare function readLottieFile(absolutePath: string): Record<string, any>;
@@ -0,0 +1,47 @@
1
+ import fs from 'fs';
2
+ import { unzipSync, strFromU8 } from 'fflate';
3
+ /**
4
+ * Reads a local animation file. Accepts plain Lottie JSON and dotLottie (.lottie)
5
+ * containers (v1 `animations/<id>.json` and v2 `a/<id>.json` layouts).
6
+ */
7
+ export function readLottieFile(absolutePath) {
8
+ const buf = fs.readFileSync(absolutePath);
9
+ // ZIP magic number "PK" -> dotLottie container
10
+ if (buf.length > 4 && buf[0] === 0x50 && buf[1] === 0x4b) {
11
+ let files;
12
+ try {
13
+ files = unzipSync(new Uint8Array(buf));
14
+ }
15
+ catch (err) {
16
+ throw new Error(`Failed to unzip .lottie file: ${err.message}`);
17
+ }
18
+ let entry;
19
+ if (files['manifest.json']) {
20
+ try {
21
+ const manifest = JSON.parse(strFromU8(files['manifest.json']));
22
+ const id = manifest?.animations?.[0]?.id;
23
+ if (id) {
24
+ entry = [`animations/${id}.json`, `a/${id}.json`].find((p) => files[p]);
25
+ }
26
+ }
27
+ catch {
28
+ // fall through to scan
29
+ }
30
+ }
31
+ entry ??= Object.keys(files).find((p) => /^(animations|a)\/[^/]+\.json$/.test(p));
32
+ if (!entry)
33
+ throw new Error('No animation found inside the .lottie file.');
34
+ try {
35
+ return JSON.parse(strFromU8(files[entry]));
36
+ }
37
+ catch (err) {
38
+ throw new Error(`Failed to parse animation inside .lottie file: ${err.message}`);
39
+ }
40
+ }
41
+ try {
42
+ return JSON.parse(buf.toString('utf-8'));
43
+ }
44
+ catch (err) {
45
+ throw new Error(`Failed to parse local JSON file: ${err.message}`);
46
+ }
47
+ }
@@ -0,0 +1,20 @@
1
+ export declare function setProjectRoot(root: string | null): void;
2
+ export declare function getProjectRoot(): string | null;
3
+ /** Returns every root the client reports (empty if unsupported). */
4
+ export declare function getClientRoots(server?: any): Promise<string[]>;
5
+ /** Kept for backwards compatibility: first client root, or null. */
6
+ export declare function getClientProjectRoot(server?: any): Promise<string | null>;
7
+ export interface ResolvePathOptions {
8
+ server?: any;
9
+ projectRoot?: string | null;
10
+ }
11
+ /** True when `child` is `parent` itself or inside it (no `..` escape). */
12
+ export declare function isInside(parent: string, child: string): boolean;
13
+ /**
14
+ * Resolves a user/AI supplied path to an absolute path that is guaranteed to stay
15
+ * inside the project root (symlinks included).
16
+ */
17
+ export declare function resolveSafePath(targetPath: string, options?: ResolvePathOptions): Promise<{
18
+ absolutePath: string;
19
+ projectRoot: string | null;
20
+ }>;
@@ -0,0 +1,116 @@
1
+ import { fileURLToPath } from 'url';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ /**
5
+ * Project-root resolution and path safety for every tool that reads or writes files.
6
+ *
7
+ * Root sources, in order of priority:
8
+ * 1. ANIMATION_MCP_ROOT env var (explicit override, for clients without `roots/list`)
9
+ * 2. The roots the MCP client reports through `roots/list` (re-read on every call,
10
+ * so switching workspaces in the editor is picked up)
11
+ * 3. Nothing -> relative paths are refused; absolute paths are only allowed
12
+ * inside the server's current working directory.
13
+ */
14
+ let rootsUnsupported = false;
15
+ let manualRoot = null;
16
+ export function setProjectRoot(root) {
17
+ manualRoot = root;
18
+ }
19
+ export function getProjectRoot() {
20
+ return manualRoot;
21
+ }
22
+ function uriToPath(uri) {
23
+ try {
24
+ if (uri.startsWith('file://'))
25
+ return fileURLToPath(uri);
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ return path.isAbsolute(uri) ? uri : null;
31
+ }
32
+ /** Returns every root the client reports (empty if unsupported). */
33
+ export async function getClientRoots(server) {
34
+ if (rootsUnsupported || !server || typeof server.listRoots !== 'function')
35
+ return [];
36
+ try {
37
+ const response = await server.listRoots();
38
+ return (response?.roots ?? [])
39
+ .map((r) => uriToPath(r.uri))
40
+ .filter((p) => !!p);
41
+ }
42
+ catch {
43
+ // Client does not implement the roots capability: do not ask again.
44
+ rootsUnsupported = true;
45
+ return [];
46
+ }
47
+ }
48
+ /** Kept for backwards compatibility: first client root, or null. */
49
+ export async function getClientProjectRoot(server) {
50
+ const roots = await getClientRoots(server);
51
+ return roots[0] ?? null;
52
+ }
53
+ /** True when `child` is `parent` itself or inside it (no `..` escape). */
54
+ export function isInside(parent, child) {
55
+ const rel = path.relative(parent, child);
56
+ return rel === '' || (!rel.startsWith('..' + path.sep) && rel !== '..' && !path.isAbsolute(rel));
57
+ }
58
+ /** realpath of the deepest existing ancestor + the not-yet-existing remainder. */
59
+ function realpathLoose(p) {
60
+ const missing = [];
61
+ let current = p;
62
+ for (;;) {
63
+ try {
64
+ const real = fs.realpathSync(current);
65
+ return missing.length ? path.join(real, ...missing.reverse()) : real;
66
+ }
67
+ catch {
68
+ const parent = path.dirname(current);
69
+ if (parent === current)
70
+ return p;
71
+ missing.push(path.basename(current));
72
+ current = parent;
73
+ }
74
+ }
75
+ }
76
+ /**
77
+ * Resolves a user/AI supplied path to an absolute path that is guaranteed to stay
78
+ * inside the project root (symlinks included).
79
+ */
80
+ export async function resolveSafePath(targetPath, options) {
81
+ const trimmed = targetPath.trim();
82
+ if (!trimmed)
83
+ throw new Error('Path cannot be empty.');
84
+ if (trimmed.includes('\0'))
85
+ throw new Error('Path contains an invalid character.');
86
+ let roots = [];
87
+ if (options?.projectRoot)
88
+ roots = [options.projectRoot];
89
+ else if (process.env.ANIMATION_MCP_ROOT)
90
+ roots = [path.resolve(process.env.ANIMATION_MCP_ROOT)];
91
+ else if (manualRoot)
92
+ roots = [manualRoot];
93
+ else
94
+ roots = await getClientRoots(options?.server);
95
+ const isAbs = path.isAbsolute(trimmed);
96
+ if (roots.length === 0) {
97
+ if (!isAbs) {
98
+ throw new Error(`Relative path "${trimmed}" provided, but no project root was detected from your MCP client. ` +
99
+ `Provide an absolute path inside your project, or set the ANIMATION_MCP_ROOT environment variable in your MCP config.`);
100
+ }
101
+ // No root known: only allow absolute paths inside the folder the server was started in.
102
+ roots = [process.cwd()];
103
+ }
104
+ const candidate = isAbs ? path.normalize(trimmed) : null;
105
+ // For absolute paths pick the root that contains it; for relative ones use the first root.
106
+ const root = candidate
107
+ ? roots.find((r) => isInside(realpathLoose(r), realpathLoose(candidate))) ?? roots[0]
108
+ : roots[0];
109
+ const resolved = candidate ?? path.resolve(root, trimmed);
110
+ const realRoot = realpathLoose(root);
111
+ const realTarget = realpathLoose(resolved);
112
+ if (!isInside(realRoot, realTarget)) {
113
+ throw new Error(`Access denied: path "${trimmed}" resolves outside project root "${root}".`);
114
+ }
115
+ return { absolutePath: resolved, projectRoot: root };
116
+ }
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
2
  "name": "@rafaylabs/animation-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
4
+ "type": "module",
5
+ "description": "On-brand Lottie animations and framework code inside your AI coding assistant (Cursor, Claude Code, Windsurf, VS Code)",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "animation-mcp": "./bin/cli.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "watch": "tsc -w",
13
+ "start": "node bin/cli.js",
14
+ "test": "tsc && node --test",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "keywords": [
18
+ "mcp",
19
+ "model-context-protocol",
20
+ "lottie",
21
+ "animation",
22
+ "ai",
23
+ "cursor",
24
+ "claude-code",
25
+ "react",
26
+ "react-native",
27
+ "nextjs",
28
+ "theming"
29
+ ],
30
+ "author": "Abdul Rafay <info@getrafaylabs.com> (https://getrafaylabs.com)",
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.32.2",
34
+ "commander": "^12.1.0",
35
+ "fflate": "^0.8.3",
36
+ "zod": "^3.23.8"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^22.10.0",
40
+ "typescript": "^5.7.0"
41
+ },
42
+ "engines": {
43
+ "node": ">=18.0.0"
44
+ },
45
+ "files": [
46
+ "dist",
47
+ "bin",
48
+ "README.md",
49
+ "LICENSE"
50
+ ],
51
+ "homepage": "https://getrafaylabs.com/animation-mcp",
52
+ "bugs": {
53
+ "email": "info@getrafaylabs.com"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public"
57
+ }
58
+ }