@rafaylabs/animation-mcp 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +111 -2
- package/bin/cli.js +50 -0
- package/dist/api/catalog-client.d.ts +27 -0
- package/dist/api/catalog-client.js +67 -0
- package/dist/api/server.d.ts +29 -0
- package/dist/api/server.js +55 -0
- package/dist/auth/cli-login.d.ts +3 -0
- package/dist/auth/cli-login.js +121 -0
- package/dist/auth/client.d.ts +45 -0
- package/dist/auth/client.js +170 -0
- package/dist/auth/config.d.ts +36 -0
- package/dist/auth/config.js +106 -0
- package/dist/auth/device-login.d.ts +2 -0
- package/dist/auth/device-login.js +63 -0
- package/dist/auth/product.d.ts +17 -0
- package/dist/auth/product.js +9 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +262 -0
- package/dist/theming/classifier.d.ts +16 -0
- package/dist/theming/classifier.js +74 -0
- package/dist/theming/color-walker.d.ts +19 -0
- package/dist/theming/color-walker.js +303 -0
- package/dist/theming/engine.d.ts +29 -0
- package/dist/theming/engine.js +144 -0
- package/dist/theming/oklch.d.ts +63 -0
- package/dist/theming/oklch.js +147 -0
- package/dist/tools/code.d.ts +20 -0
- package/dist/tools/code.js +104 -0
- package/dist/tools/get.d.ts +13 -0
- package/dist/tools/get.js +10 -0
- package/dist/tools/save.d.ts +22 -0
- package/dist/tools/save.js +78 -0
- package/dist/tools/schemas.d.ts +137 -0
- package/dist/tools/schemas.js +50 -0
- package/dist/tools/search.d.ts +29 -0
- package/dist/tools/search.js +22 -0
- package/dist/tools/theme.d.ts +28 -0
- package/dist/tools/theme.js +66 -0
- package/dist/utils/lottie-file.d.ts +5 -0
- package/dist/utils/lottie-file.js +47 -0
- package/dist/utils/workspace.d.ts +20 -0
- package/dist/utils/workspace.js +116 -0
- package/package.json +56 -4
|
@@ -0,0 +1,137 @@
|
|
|
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
|
+
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
limit: number;
|
|
11
|
+
query: string;
|
|
12
|
+
category?: string | undefined;
|
|
13
|
+
style?: string | undefined;
|
|
14
|
+
loop?: boolean | undefined;
|
|
15
|
+
max_size_kb?: number | undefined;
|
|
16
|
+
}, {
|
|
17
|
+
query: string;
|
|
18
|
+
limit?: number | undefined;
|
|
19
|
+
category?: string | undefined;
|
|
20
|
+
style?: string | undefined;
|
|
21
|
+
loop?: boolean | undefined;
|
|
22
|
+
max_size_kb?: number | undefined;
|
|
23
|
+
}>;
|
|
24
|
+
export declare const GetAnimationArgsSchema: z.ZodObject<{
|
|
25
|
+
id: z.ZodString;
|
|
26
|
+
}, "strip", z.ZodTypeAny, {
|
|
27
|
+
id: string;
|
|
28
|
+
}, {
|
|
29
|
+
id: string;
|
|
30
|
+
}>;
|
|
31
|
+
export declare const ApplyThemeArgsSchema: z.ZodObject<{
|
|
32
|
+
id: z.ZodOptional<z.ZodString>;
|
|
33
|
+
file_path: z.ZodOptional<z.ZodString>;
|
|
34
|
+
lottie_json: z.ZodOptional<z.ZodUnion<[z.ZodRecord<z.ZodString, z.ZodAny>, z.ZodString]>>;
|
|
35
|
+
output_path: z.ZodOptional<z.ZodString>;
|
|
36
|
+
format: z.ZodOptional<z.ZodEnum<["json", "lottie"]>>;
|
|
37
|
+
overwrite: z.ZodOptional<z.ZodBoolean>;
|
|
38
|
+
theme: z.ZodObject<{
|
|
39
|
+
primary: z.ZodString;
|
|
40
|
+
secondary: z.ZodOptional<z.ZodString>;
|
|
41
|
+
background: z.ZodOptional<z.ZodString>;
|
|
42
|
+
accent: z.ZodOptional<z.ZodString>;
|
|
43
|
+
}, "strip", z.ZodTypeAny, {
|
|
44
|
+
primary: string;
|
|
45
|
+
secondary?: string | undefined;
|
|
46
|
+
background?: string | undefined;
|
|
47
|
+
accent?: string | undefined;
|
|
48
|
+
}, {
|
|
49
|
+
primary: string;
|
|
50
|
+
secondary?: string | undefined;
|
|
51
|
+
background?: string | undefined;
|
|
52
|
+
accent?: string | undefined;
|
|
53
|
+
}>;
|
|
54
|
+
mode: z.ZodOptional<z.ZodEnum<["light", "dark"]>>;
|
|
55
|
+
}, "strip", z.ZodTypeAny, {
|
|
56
|
+
theme: {
|
|
57
|
+
primary: string;
|
|
58
|
+
secondary?: string | undefined;
|
|
59
|
+
background?: string | undefined;
|
|
60
|
+
accent?: string | undefined;
|
|
61
|
+
};
|
|
62
|
+
mode?: "light" | "dark" | undefined;
|
|
63
|
+
id?: string | undefined;
|
|
64
|
+
lottie_json?: string | Record<string, any> | undefined;
|
|
65
|
+
format?: "json" | "lottie" | undefined;
|
|
66
|
+
file_path?: string | undefined;
|
|
67
|
+
output_path?: string | undefined;
|
|
68
|
+
overwrite?: boolean | undefined;
|
|
69
|
+
}, {
|
|
70
|
+
theme: {
|
|
71
|
+
primary: string;
|
|
72
|
+
secondary?: string | undefined;
|
|
73
|
+
background?: string | undefined;
|
|
74
|
+
accent?: string | undefined;
|
|
75
|
+
};
|
|
76
|
+
mode?: "light" | "dark" | undefined;
|
|
77
|
+
id?: string | undefined;
|
|
78
|
+
lottie_json?: string | Record<string, any> | undefined;
|
|
79
|
+
format?: "json" | "lottie" | undefined;
|
|
80
|
+
file_path?: string | undefined;
|
|
81
|
+
output_path?: string | undefined;
|
|
82
|
+
overwrite?: boolean | undefined;
|
|
83
|
+
}>;
|
|
84
|
+
export declare const SaveAnimationArgsSchema: z.ZodObject<{
|
|
85
|
+
themed_json: z.ZodUnion<[z.ZodRecord<z.ZodString, z.ZodAny>, z.ZodString]>;
|
|
86
|
+
path: z.ZodString;
|
|
87
|
+
format: z.ZodOptional<z.ZodEnum<["json", "lottie"]>>;
|
|
88
|
+
overwrite: z.ZodOptional<z.ZodBoolean>;
|
|
89
|
+
}, "strip", z.ZodTypeAny, {
|
|
90
|
+
path: string;
|
|
91
|
+
themed_json: string | Record<string, any>;
|
|
92
|
+
format?: "json" | "lottie" | undefined;
|
|
93
|
+
overwrite?: boolean | undefined;
|
|
94
|
+
}, {
|
|
95
|
+
path: string;
|
|
96
|
+
themed_json: string | Record<string, any>;
|
|
97
|
+
format?: "json" | "lottie" | undefined;
|
|
98
|
+
overwrite?: boolean | undefined;
|
|
99
|
+
}>;
|
|
100
|
+
export declare const GetCodeArgsSchema: z.ZodObject<{
|
|
101
|
+
framework: z.ZodEnum<["react", "react-native", "nextjs", "html"]>;
|
|
102
|
+
file_path: z.ZodString;
|
|
103
|
+
options: z.ZodOptional<z.ZodObject<{
|
|
104
|
+
loop: z.ZodOptional<z.ZodBoolean>;
|
|
105
|
+
autoplay: z.ZodOptional<z.ZodBoolean>;
|
|
106
|
+
size: z.ZodOptional<z.ZodNumber>;
|
|
107
|
+
componentName: z.ZodOptional<z.ZodString>;
|
|
108
|
+
}, "strip", z.ZodTypeAny, {
|
|
109
|
+
loop?: boolean | undefined;
|
|
110
|
+
autoplay?: boolean | undefined;
|
|
111
|
+
size?: number | undefined;
|
|
112
|
+
componentName?: string | undefined;
|
|
113
|
+
}, {
|
|
114
|
+
loop?: boolean | undefined;
|
|
115
|
+
autoplay?: boolean | undefined;
|
|
116
|
+
size?: number | undefined;
|
|
117
|
+
componentName?: string | undefined;
|
|
118
|
+
}>>;
|
|
119
|
+
}, "strip", z.ZodTypeAny, {
|
|
120
|
+
file_path: string;
|
|
121
|
+
framework: "react" | "react-native" | "nextjs" | "html";
|
|
122
|
+
options?: {
|
|
123
|
+
loop?: boolean | undefined;
|
|
124
|
+
autoplay?: boolean | undefined;
|
|
125
|
+
size?: number | undefined;
|
|
126
|
+
componentName?: string | undefined;
|
|
127
|
+
} | undefined;
|
|
128
|
+
}, {
|
|
129
|
+
file_path: string;
|
|
130
|
+
framework: "react" | "react-native" | "nextjs" | "html";
|
|
131
|
+
options?: {
|
|
132
|
+
loop?: boolean | undefined;
|
|
133
|
+
autoplay?: boolean | undefined;
|
|
134
|
+
size?: number | undefined;
|
|
135
|
+
componentName?: string | undefined;
|
|
136
|
+
} | undefined;
|
|
137
|
+
}>;
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
});
|
|
10
|
+
export const GetAnimationArgsSchema = z.object({
|
|
11
|
+
id: z.string().regex(/^[A-Za-z0-9_-]{1,128}$/, 'Animation ID may only contain letters, numbers, - and _'),
|
|
12
|
+
});
|
|
13
|
+
const HexColorRegex = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
14
|
+
export const ApplyThemeArgsSchema = z.object({
|
|
15
|
+
id: z.string().regex(/^[A-Za-z0-9_-]{1,128}$/, 'Animation ID may only contain letters, numbers, - and _').optional(),
|
|
16
|
+
file_path: z.string().optional(),
|
|
17
|
+
lottie_json: z.union([z.record(z.any()), z.string()]).optional(),
|
|
18
|
+
output_path: z.string().optional(),
|
|
19
|
+
format: z.enum(['json', 'lottie']).optional(),
|
|
20
|
+
overwrite: z.boolean().optional(),
|
|
21
|
+
theme: z.object({
|
|
22
|
+
primary: z.string().regex(HexColorRegex, 'Primary color must be a valid hex string (e.g. #0476FF)'),
|
|
23
|
+
secondary: z.string().regex(HexColorRegex, 'Secondary color must be a valid hex string').optional(),
|
|
24
|
+
background: z.string().regex(HexColorRegex, 'Background color must be a valid hex string').optional(),
|
|
25
|
+
accent: z.string().regex(HexColorRegex, 'Accent color must be a valid hex string').optional(),
|
|
26
|
+
}),
|
|
27
|
+
mode: z.enum(['light', 'dark']).optional(),
|
|
28
|
+
});
|
|
29
|
+
export const SaveAnimationArgsSchema = z.object({
|
|
30
|
+
themed_json: z.union([z.record(z.any()), z.string()], {
|
|
31
|
+
required_error: 'themed_json is required',
|
|
32
|
+
}),
|
|
33
|
+
path: z.string().min(1, 'Target file path is required'),
|
|
34
|
+
format: z.enum(['json', 'lottie']).optional(),
|
|
35
|
+
overwrite: z.boolean().optional(),
|
|
36
|
+
});
|
|
37
|
+
export const GetCodeArgsSchema = z.object({
|
|
38
|
+
framework: z.enum(['react', 'react-native', 'nextjs', 'html'], {
|
|
39
|
+
errorMap: () => ({ message: 'Framework must be one of: react, react-native, nextjs, html' }),
|
|
40
|
+
}),
|
|
41
|
+
file_path: z.string().min(1, 'file_path is required'),
|
|
42
|
+
options: z
|
|
43
|
+
.object({
|
|
44
|
+
loop: z.boolean().optional(),
|
|
45
|
+
autoplay: z.boolean().optional(),
|
|
46
|
+
size: z.number().positive().optional(),
|
|
47
|
+
componentName: z.string().optional(),
|
|
48
|
+
})
|
|
49
|
+
.optional(),
|
|
50
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
}, "strip", z.ZodTypeAny, {
|
|
11
|
+
limit: number;
|
|
12
|
+
query: string;
|
|
13
|
+
category?: string | undefined;
|
|
14
|
+
style?: string | undefined;
|
|
15
|
+
loop?: boolean | undefined;
|
|
16
|
+
max_size_kb?: number | undefined;
|
|
17
|
+
}, {
|
|
18
|
+
query: string;
|
|
19
|
+
limit?: number | undefined;
|
|
20
|
+
category?: string | undefined;
|
|
21
|
+
style?: string | undefined;
|
|
22
|
+
loop?: boolean | undefined;
|
|
23
|
+
max_size_kb?: number | undefined;
|
|
24
|
+
}>;
|
|
25
|
+
export type SearchAnimationsInput = z.infer<typeof SearchAnimationsSchema>;
|
|
26
|
+
export declare function handleSearchAnimations(input: SearchAnimationsInput): Promise<{
|
|
27
|
+
results: CatalogSearchResultItem[];
|
|
28
|
+
totalMatches: number;
|
|
29
|
+
}>;
|
|
@@ -0,0 +1,22 @@
|
|
|
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('Maximum number of results to return (default 5)'),
|
|
10
|
+
});
|
|
11
|
+
export async function handleSearchAnimations(input) {
|
|
12
|
+
// Sign-in, plan and daily limits are enforced by the server; it answers 401/429 with a clear message.
|
|
13
|
+
const results = await searchCatalog({
|
|
14
|
+
query: input.query,
|
|
15
|
+
category: input.category,
|
|
16
|
+
style: input.style,
|
|
17
|
+
loop: input.loop,
|
|
18
|
+
max_size_kb: input.max_size_kb,
|
|
19
|
+
limit: input.limit,
|
|
20
|
+
});
|
|
21
|
+
return { results, totalMatches: results.length };
|
|
22
|
+
}
|
|
@@ -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,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": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
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
|
+
}
|