@imgcli/mcp 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +28 -2
- package/THIRD_PARTY_NOTICES +26 -0
- package/catalog.json +70 -0
- package/dist/access.js +60 -0
- package/dist/catalog-schema.js +115 -0
- package/dist/catalog.js +144 -0
- package/dist/index.js +100 -0
- package/dist/rules.js +22 -0
- package/package.json +35 -4
package/dist/access.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { PUBLIC_ORIGIN } from "./catalog-schema.js";
|
|
2
|
+
const ACCESS_TTL_MS = 15 * 60_000;
|
|
3
|
+
export class McpToolError extends Error {
|
|
4
|
+
}
|
|
5
|
+
export function serviceOrigin(value = process.env.IMGCLI_MCP_ORIGIN) {
|
|
6
|
+
if (!value || value === PUBLIC_ORIGIN)
|
|
7
|
+
return PUBLIC_ORIGIN;
|
|
8
|
+
const local = /^http:\/\/(?:127\.0\.0\.1|\[::1\]):\d{1,5}$/;
|
|
9
|
+
if (!local.test(value))
|
|
10
|
+
throw new Error("IMGCLI_MCP_ORIGIN must be the Imgcli site or a local test server.");
|
|
11
|
+
return new URL(value).origin;
|
|
12
|
+
}
|
|
13
|
+
export function createAccessCheck(origin, key) {
|
|
14
|
+
let expiresAt = 0;
|
|
15
|
+
let pending;
|
|
16
|
+
async function verify() {
|
|
17
|
+
if (!key || key === "YOUR_KEY") {
|
|
18
|
+
throw new McpToolError("Set IMGCLI_MCP_API_KEY to an MCP key from https://imgcli.com/account.");
|
|
19
|
+
}
|
|
20
|
+
let response;
|
|
21
|
+
try {
|
|
22
|
+
response = await fetch(new URL("/api/mcp/access", origin), {
|
|
23
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
24
|
+
signal: AbortSignal.timeout(5000),
|
|
25
|
+
redirect: "error",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new McpToolError("Could not reach Imgcli to verify your key. Check your connection and retry.");
|
|
30
|
+
}
|
|
31
|
+
const result = await response.json().catch(() => null);
|
|
32
|
+
if (response.status === 401) {
|
|
33
|
+
throw new McpToolError("This key is invalid or revoked. Create an MCP key at https://imgcli.com/account.");
|
|
34
|
+
}
|
|
35
|
+
if (response.status === 403) {
|
|
36
|
+
throw new McpToolError("Use an MCP key. Create one under API keys at https://imgcli.com/account.");
|
|
37
|
+
}
|
|
38
|
+
if (response.status === 429) {
|
|
39
|
+
throw new McpToolError("Imgcli is rate limiting key checks. Wait a minute and retry.");
|
|
40
|
+
}
|
|
41
|
+
if (!response.ok ||
|
|
42
|
+
!result ||
|
|
43
|
+
typeof result !== "object" ||
|
|
44
|
+
!("status" in result) ||
|
|
45
|
+
result.status !== "allowed" ||
|
|
46
|
+
!("scope" in result) ||
|
|
47
|
+
result.scope !== "mcp") {
|
|
48
|
+
throw new McpToolError("Imgcli could not verify your key. Try again shortly.");
|
|
49
|
+
}
|
|
50
|
+
expiresAt = Date.now() + ACCESS_TTL_MS;
|
|
51
|
+
}
|
|
52
|
+
return async () => {
|
|
53
|
+
if (Date.now() < expiresAt)
|
|
54
|
+
return;
|
|
55
|
+
pending ??= verify().finally(() => {
|
|
56
|
+
pending = undefined;
|
|
57
|
+
});
|
|
58
|
+
await pending;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export const CATALOG_SCHEMA_VERSION = 1;
|
|
4
|
+
export const PUBLIC_ORIGIN = "https://imgcli.com";
|
|
5
|
+
const slug = z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/);
|
|
6
|
+
const count = z.number().int().positive();
|
|
7
|
+
const sourceHash = z.string().regex(/^[a-f0-9]{12}$/);
|
|
8
|
+
const target = z
|
|
9
|
+
.string()
|
|
10
|
+
.regex(/^(?:components\/imgcli|lib)\/[a-zA-Z0-9_/-]+\.(?:ts|tsx)$/);
|
|
11
|
+
const measurement = z.object({
|
|
12
|
+
sourceHash,
|
|
13
|
+
width: count,
|
|
14
|
+
height: count,
|
|
15
|
+
fps: count,
|
|
16
|
+
durationInFrames: count,
|
|
17
|
+
settle: z.object({
|
|
18
|
+
base: z.number(),
|
|
19
|
+
baseWords: z.number(),
|
|
20
|
+
perWord: z.number(),
|
|
21
|
+
}),
|
|
22
|
+
copy: z
|
|
23
|
+
.object({
|
|
24
|
+
prop: z.string(),
|
|
25
|
+
pxPerChar: z.number().positive(),
|
|
26
|
+
maxChars: count,
|
|
27
|
+
})
|
|
28
|
+
.optional(),
|
|
29
|
+
});
|
|
30
|
+
const prop = z.object({
|
|
31
|
+
name: z.string(),
|
|
32
|
+
type: z.string(),
|
|
33
|
+
required: z.boolean(),
|
|
34
|
+
defaultExpression: z.string().optional(),
|
|
35
|
+
});
|
|
36
|
+
export const componentSchema = z.object({
|
|
37
|
+
slug,
|
|
38
|
+
title: z.string(),
|
|
39
|
+
description: z.string(),
|
|
40
|
+
category: slug,
|
|
41
|
+
tier: z.literal("free"),
|
|
42
|
+
aliases: z.array(slug),
|
|
43
|
+
docsPath: z.string().regex(/^\/docs(?:\/[a-z0-9-]+)*$/),
|
|
44
|
+
notes: z.string().optional(),
|
|
45
|
+
exportName: z.string().regex(/^[A-Z][a-zA-Z0-9]*$/),
|
|
46
|
+
target,
|
|
47
|
+
importPath: z.string().regex(/^@\/components\/imgcli\/[a-z0-9-]+$/),
|
|
48
|
+
files: z.array(target).nonempty(),
|
|
49
|
+
dependencies: z.array(z.string()),
|
|
50
|
+
registryDependencies: z.array(z.string()),
|
|
51
|
+
props: z.array(prop).nonempty(),
|
|
52
|
+
preview: z
|
|
53
|
+
.object({
|
|
54
|
+
width: count,
|
|
55
|
+
height: count,
|
|
56
|
+
fps: count,
|
|
57
|
+
durationInFrames: count,
|
|
58
|
+
controls: z.record(z.string(), z.object({
|
|
59
|
+
type: z.string(),
|
|
60
|
+
label: z.string(),
|
|
61
|
+
default: z.union([z.string(), z.number(), z.boolean()]),
|
|
62
|
+
min: z.number().optional(),
|
|
63
|
+
max: z.number().optional(),
|
|
64
|
+
step: z.number().optional(),
|
|
65
|
+
options: z.array(z.string()).optional(),
|
|
66
|
+
})),
|
|
67
|
+
example: z.string().optional(),
|
|
68
|
+
})
|
|
69
|
+
.optional(),
|
|
70
|
+
sourceHash,
|
|
71
|
+
installHash: z.string().regex(/^[a-f0-9]{16}$/),
|
|
72
|
+
measurement: measurement.optional(),
|
|
73
|
+
});
|
|
74
|
+
export const catalogSchema = z
|
|
75
|
+
.object({
|
|
76
|
+
schemaVersion: z.literal(CATALOG_SCHEMA_VERSION),
|
|
77
|
+
revision: z.string().regex(/^[a-f0-9]{16}$/),
|
|
78
|
+
components: z.array(componentSchema).nonempty(),
|
|
79
|
+
})
|
|
80
|
+
.superRefine(({ components }, ctx) => {
|
|
81
|
+
const names = new Set();
|
|
82
|
+
for (const component of components) {
|
|
83
|
+
for (const name of [component.slug, ...component.aliases]) {
|
|
84
|
+
if (names.has(name))
|
|
85
|
+
ctx.addIssue({ code: "custom", message: `Duplicate name: ${name}` });
|
|
86
|
+
names.add(name);
|
|
87
|
+
}
|
|
88
|
+
if (!component.files.includes(component.target)) {
|
|
89
|
+
ctx.addIssue({
|
|
90
|
+
code: "custom",
|
|
91
|
+
message: `Missing entry file: ${component.slug}`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (component.measurement &&
|
|
95
|
+
component.measurement.sourceHash !== component.sourceHash) {
|
|
96
|
+
ctx.addIssue({
|
|
97
|
+
code: "custom",
|
|
98
|
+
message: `Stale measurement: ${component.slug}`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
export function catalogRevision(components) {
|
|
104
|
+
return createHash("sha256")
|
|
105
|
+
.update(JSON.stringify(components))
|
|
106
|
+
.digest("hex")
|
|
107
|
+
.slice(0, 16);
|
|
108
|
+
}
|
|
109
|
+
export function parseCatalog(data) {
|
|
110
|
+
const catalog = catalogSchema.parse(data);
|
|
111
|
+
if (catalogRevision(catalog.components) !== catalog.revision) {
|
|
112
|
+
throw new Error("Catalog contents do not match their revision.");
|
|
113
|
+
}
|
|
114
|
+
return catalog;
|
|
115
|
+
}
|
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { PUBLIC_ORIGIN, parseCatalog, } from "./catalog-schema.js";
|
|
6
|
+
const REFRESH_MS = 5 * 60_000;
|
|
7
|
+
const RETRY_MS = 30_000;
|
|
8
|
+
const bundled = new URL("../catalog.json", import.meta.url);
|
|
9
|
+
export function createCatalogReader(origin, cacheRoot = path.join(homedir(), ".cache", "imgcli-mcp")) {
|
|
10
|
+
const cacheDir = path.join(cacheRoot, new URL(origin).host.replace(/[^a-zA-Z0-9.-]/g, "_"));
|
|
11
|
+
const cache = path.join(cacheDir, "catalog-v1.json");
|
|
12
|
+
let current;
|
|
13
|
+
let pending;
|
|
14
|
+
async function refresh() {
|
|
15
|
+
try {
|
|
16
|
+
const response = await fetch(new URL("/api/mcp/catalog", origin), {
|
|
17
|
+
signal: AbortSignal.timeout(5000),
|
|
18
|
+
redirect: "error",
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok)
|
|
21
|
+
throw new Error("Directory unavailable");
|
|
22
|
+
const catalog = parseCatalog(await response.json());
|
|
23
|
+
const tmp = path.join(cacheDir, `${randomUUID()}.tmp`);
|
|
24
|
+
try {
|
|
25
|
+
await mkdir(cacheDir, { recursive: true, mode: 0o700 });
|
|
26
|
+
await writeFile(tmp, JSON.stringify(catalog), {
|
|
27
|
+
flag: "wx",
|
|
28
|
+
mode: 0o600,
|
|
29
|
+
});
|
|
30
|
+
await rename(tmp, cache);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
await rm(tmp, { force: true }).catch(() => { });
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
catalog,
|
|
37
|
+
from: "live",
|
|
38
|
+
expiresAt: Date.now() + REFRESH_MS,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
const cached = current?.from !== "bundled" && current?.catalog
|
|
43
|
+
? current.catalog
|
|
44
|
+
: await readFile(cache, "utf8")
|
|
45
|
+
.then((s) => parseCatalog(JSON.parse(s)))
|
|
46
|
+
.catch(() => null);
|
|
47
|
+
const catalog = cached ?? parseCatalog(JSON.parse(await readFile(bundled, "utf8")));
|
|
48
|
+
return {
|
|
49
|
+
catalog,
|
|
50
|
+
from: cached ? "cache" : "bundled",
|
|
51
|
+
expiresAt: Date.now() + RETRY_MS,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return async () => {
|
|
56
|
+
if (current && Date.now() < current.expiresAt)
|
|
57
|
+
return current;
|
|
58
|
+
pending ??= refresh().finally(() => {
|
|
59
|
+
pending = undefined;
|
|
60
|
+
});
|
|
61
|
+
current = await pending;
|
|
62
|
+
return current;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const normalize = (value) => value
|
|
66
|
+
.trim()
|
|
67
|
+
.toLowerCase()
|
|
68
|
+
.replace(/^@(?:imgcli|snapcn)\//, "")
|
|
69
|
+
.replace(/[\s_]+/g, "-");
|
|
70
|
+
export function findComponent(catalog, name) {
|
|
71
|
+
const slug = normalize(name);
|
|
72
|
+
return catalog.components.find((item) => item.slug === slug ||
|
|
73
|
+
normalize(item.title) === slug ||
|
|
74
|
+
item.aliases.includes(slug));
|
|
75
|
+
}
|
|
76
|
+
export function summarize(item) {
|
|
77
|
+
return {
|
|
78
|
+
slug: item.slug,
|
|
79
|
+
title: item.title,
|
|
80
|
+
category: item.category,
|
|
81
|
+
tier: item.tier,
|
|
82
|
+
description: item.description,
|
|
83
|
+
fps: item.preview?.fps ?? null,
|
|
84
|
+
durationInFrames: item.preview?.durationInFrames ?? null,
|
|
85
|
+
notes: item.notes,
|
|
86
|
+
docs: new URL(item.docsPath, PUBLIC_ORIGIN).href,
|
|
87
|
+
install: `npx shadcn@latest add ${PUBLIC_ORIGIN}/r/${item.slug}.json`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const STOP_WORDS = new Set("a an and at by for from i in into is it me my of on or that the to using want with".split(" "));
|
|
91
|
+
const tokens = (text) => new Set(text
|
|
92
|
+
.toLowerCase()
|
|
93
|
+
.match(/[\p{L}\p{N}]+/gu)
|
|
94
|
+
?.filter((token) => token.length > 1 && !STOP_WORDS.has(token)));
|
|
95
|
+
export function searchComponents(catalog, input) {
|
|
96
|
+
const { query, category, vibe, tier, max_frames, limit } = input;
|
|
97
|
+
const candidates = catalog.components.filter((item) => (!category || item.category === normalize(category)) &&
|
|
98
|
+
(!tier || item.tier === tier) &&
|
|
99
|
+
(max_frames === undefined ||
|
|
100
|
+
(item.preview !== undefined &&
|
|
101
|
+
item.preview.durationInFrames <= max_frames)));
|
|
102
|
+
const terms = tokens(query);
|
|
103
|
+
const mood = tokens(vibe ?? "");
|
|
104
|
+
const matches = candidates
|
|
105
|
+
.map((item) => {
|
|
106
|
+
const name = tokens(`${item.slug} ${item.title}`);
|
|
107
|
+
const description = tokens(item.description);
|
|
108
|
+
let score = normalize(query) === item.slug ? 20 : 0;
|
|
109
|
+
for (const term of terms) {
|
|
110
|
+
if (name.has(term))
|
|
111
|
+
score += 5;
|
|
112
|
+
if (description.has(term))
|
|
113
|
+
score += 2;
|
|
114
|
+
}
|
|
115
|
+
for (const term of mood) {
|
|
116
|
+
if (name.has(term) || description.has(term))
|
|
117
|
+
score++;
|
|
118
|
+
}
|
|
119
|
+
return { item, score };
|
|
120
|
+
})
|
|
121
|
+
.filter(({ score }) => (!query.trim() && !mood.size) || score > 0)
|
|
122
|
+
.sort((a, b) => b.score - a.score || a.item.slug.localeCompare(b.item.slug, "en"));
|
|
123
|
+
if (matches.length) {
|
|
124
|
+
return {
|
|
125
|
+
matches: matches.slice(0, limit).map(({ item }) => summarize(item)),
|
|
126
|
+
categories: !query.trim()
|
|
127
|
+
? [...new Set(candidates.map(({ category }) => category))].sort()
|
|
128
|
+
: undefined,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const alternatives = [
|
|
132
|
+
"text-reveal",
|
|
133
|
+
"punch-lines",
|
|
134
|
+
"screen-recording",
|
|
135
|
+
].flatMap((slug) => {
|
|
136
|
+
const item = candidates.find((entry) => entry.slug === slug);
|
|
137
|
+
return item ? [summarize(item)] : [];
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
matches: [],
|
|
141
|
+
message: "No free components matched. Try another term or relax the filters.",
|
|
142
|
+
alternatives,
|
|
143
|
+
};
|
|
144
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { createAccessCheck, McpToolError, serviceOrigin } from "./access.js";
|
|
7
|
+
import { createCatalogReader, findComponent, searchComponents, summarize, } from "./catalog.js";
|
|
8
|
+
import { PUBLIC_ORIGIN } from "./catalog-schema.js";
|
|
9
|
+
import { RULES } from "./rules.js";
|
|
10
|
+
const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
11
|
+
const text = (value) => ({ type: "text", text: value });
|
|
12
|
+
const success = (value) => ({
|
|
13
|
+
content: [text(JSON.stringify(value))],
|
|
14
|
+
structuredContent: value,
|
|
15
|
+
});
|
|
16
|
+
async function main() {
|
|
17
|
+
const server = new McpServer({ name: "imgcli", version });
|
|
18
|
+
const origin = serviceOrigin();
|
|
19
|
+
const checkAccess = createAccessCheck(origin, process.env.IMGCLI_MCP_API_KEY);
|
|
20
|
+
const readCatalog = createCatalogReader(origin);
|
|
21
|
+
const verified = async (fn) => {
|
|
22
|
+
try {
|
|
23
|
+
await checkAccess();
|
|
24
|
+
return success(await fn());
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
return {
|
|
28
|
+
isError: true,
|
|
29
|
+
content: [
|
|
30
|
+
text(error instanceof McpToolError
|
|
31
|
+
? error.message
|
|
32
|
+
: "Imgcli could not load the tool data. Retry the call or reinstall the MCP package."),
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const annotations = { readOnlyHint: true };
|
|
38
|
+
server.registerTool("imgcli_search_components", {
|
|
39
|
+
description: "Find free Imgcli Remotion components by scene intent. An empty query lists available categories and components.",
|
|
40
|
+
annotations,
|
|
41
|
+
inputSchema: {
|
|
42
|
+
query: z.string().max(500),
|
|
43
|
+
category: z.string().max(80).optional(),
|
|
44
|
+
vibe: z.string().max(160).optional(),
|
|
45
|
+
tier: z.enum(["free", "pro"]).optional(),
|
|
46
|
+
max_frames: z.number().int().positive().optional(),
|
|
47
|
+
limit: z.number().int().min(1).max(12).default(5),
|
|
48
|
+
},
|
|
49
|
+
}, (input) => verified(async () => {
|
|
50
|
+
const { catalog, from } = await readCatalog();
|
|
51
|
+
return {
|
|
52
|
+
revision: catalog.revision,
|
|
53
|
+
source: from,
|
|
54
|
+
...searchComponents(catalog, input),
|
|
55
|
+
};
|
|
56
|
+
}));
|
|
57
|
+
server.registerTool("imgcli_component", {
|
|
58
|
+
description: "Read a component's actual TypeScript props, preview defaults, dependencies, and shadcn installation target.",
|
|
59
|
+
annotations,
|
|
60
|
+
inputSchema: { name: z.string().trim().min(1).max(120) },
|
|
61
|
+
}, ({ name }) => verified(async () => {
|
|
62
|
+
const { catalog, from } = await readCatalog();
|
|
63
|
+
const item = findComponent(catalog, name);
|
|
64
|
+
if (!item)
|
|
65
|
+
throw new McpToolError("This component is absent from the free catalog. Use imgcli_search_components to find an available component.");
|
|
66
|
+
return {
|
|
67
|
+
revision: catalog.revision,
|
|
68
|
+
source: from,
|
|
69
|
+
...summarize(item),
|
|
70
|
+
aliases: item.aliases,
|
|
71
|
+
import: `import { ${item.exportName} } from "${item.importPath}";`,
|
|
72
|
+
target: item.target,
|
|
73
|
+
files: item.files,
|
|
74
|
+
dependencies: item.dependencies,
|
|
75
|
+
registryDependencies: item.registryDependencies,
|
|
76
|
+
props: item.props,
|
|
77
|
+
preview: item.preview ?? null,
|
|
78
|
+
measurement: item.measurement ?? null,
|
|
79
|
+
sourceCode: `${PUBLIC_ORIGIN}/r/${item.slug}.json`,
|
|
80
|
+
};
|
|
81
|
+
}));
|
|
82
|
+
server.registerTool("imgcli_rules", {
|
|
83
|
+
description: "Get Imgcli's motion, design, shot structure, and render rules for Remotion scenes.",
|
|
84
|
+
annotations,
|
|
85
|
+
inputSchema: {
|
|
86
|
+
topic: z
|
|
87
|
+
.enum(["all", "motion", "design", "anatomy", "anti-patterns"])
|
|
88
|
+
.default("all"),
|
|
89
|
+
},
|
|
90
|
+
}, ({ topic }) => verified(async () => ({
|
|
91
|
+
rules: topic === "all" ? RULES : { [topic]: RULES[topic] },
|
|
92
|
+
})));
|
|
93
|
+
await server.connect(new StdioServerTransport());
|
|
94
|
+
}
|
|
95
|
+
main().catch((error) => {
|
|
96
|
+
console.error(error instanceof Error && error.message.startsWith("IMGCLI_MCP_ORIGIN")
|
|
97
|
+
? error.message
|
|
98
|
+
: "Imgcli MCP could not start.");
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
});
|
package/dist/rules.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const RULES = {
|
|
2
|
+
motion: [
|
|
3
|
+
"Drive animation with useCurrentFrame() and useVideoConfig() so frames render correctly on their own.",
|
|
4
|
+
"Reserve enough frames for the motion to settle. Inspect the beginning, the midpoint and the final pose in a real render.",
|
|
5
|
+
"When scaling type, pivot on its measured baseline. Check the rendered frames for position jumps and avoid will-change on animated text.",
|
|
6
|
+
],
|
|
7
|
+
design: [
|
|
8
|
+
"Set foreground and background together so text stays readable. Use concrete colors and explicit font families inside the composition.",
|
|
9
|
+
"Reuse the component's theme props and let users override copy, colors and media. Check the result with their actual text and aspect ratio.",
|
|
10
|
+
"Use staticFile() for project media in public/. Use a persistent HTTPS URL for remote media you expect a renderer to load.",
|
|
11
|
+
],
|
|
12
|
+
anatomy: [
|
|
13
|
+
"Start each shot with one job: introduce the subject, show an action, reveal the result, or give the viewer a next step.",
|
|
14
|
+
"Check the real component props and installation target before writing an import. Keep the composition clock consistent across the shots.",
|
|
15
|
+
"Budget frames for both holds and motion. Subtract overlap when clips share a transition.",
|
|
16
|
+
],
|
|
17
|
+
"anti-patterns": [
|
|
18
|
+
"Avoid wall-clock time, randomness and browser-driven keyframes for render timing.",
|
|
19
|
+
"Avoid website CSS variables in Remotion unless the composition supplies them explicitly.",
|
|
20
|
+
"Avoid judging a render from the live browser player alone. Inspect rendered frames with the final media and copy.",
|
|
21
|
+
],
|
|
22
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imgcli/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Imgcli's local MCP server for Remotion components",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/imgcli/mcp.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=22"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"imgcli-mcp": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"catalog.json",
|
|
20
|
+
"THIRD_PARTY_NOTICES"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.json",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:update": "npm run build && node scripts/test-update.mjs",
|
|
26
|
+
"prepack": "npm run build && node scripts/check-catalog.mjs"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "1.32.2",
|
|
30
|
+
"zod": "^4.4.3"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^24.13.3",
|
|
34
|
+
"typescript": "^5",
|
|
35
|
+
"vitest": "^4.1.9"
|
|
36
|
+
}
|
|
37
|
+
}
|