threenative-animation-mcp 0.1.2 → 0.1.3
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/dist/index.js +15 -17
- package/package.json +2 -5
- package/dist/server.js +0 -160
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "threenative-animation-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Author, repair and verify character animation through one MCP: inspect rigs, attach props, edit and verify clips, and open a calibrator view.",
|
|
6
6
|
"keywords": [
|
|
@@ -22,10 +22,7 @@
|
|
|
22
22
|
"directory": "packages/animation-mcp"
|
|
23
23
|
},
|
|
24
24
|
"type": "module",
|
|
25
|
-
"files": [
|
|
26
|
-
"dist",
|
|
27
|
-
"README.md"
|
|
28
|
-
],
|
|
25
|
+
"files": ["dist", "README.md"],
|
|
29
26
|
"main": "./dist/index.js",
|
|
30
27
|
"module": "./dist/index.js",
|
|
31
28
|
"types": "./dist/index.d.ts",
|
package/dist/server.js
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { readdir, readFile, mkdir, writeFile } from 'fs/promises';
|
|
3
|
-
import { createServer } from 'http';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
|
-
|
|
7
|
-
// src/page.ts
|
|
8
|
-
var CSS = String.raw`
|
|
9
|
-
:root{color-scheme:dark;font-family:"IBM Plex Mono","Azeret Mono",ui-monospace,monospace;background:#080b0a;color:#dce4de;--line:#27302c;--line2:#38433e;--panel:#111613;--panel2:#171d1a;--acid:#b7f34a;--cyan:#68d9d0;--muted:#77837c;--red:#ff705d}*{box-sizing:border-box}body{margin:0;overflow:hidden;background:#080b0a}button{cursor:pointer}.lab{height:100vh;display:grid;grid-template-rows:50px 1fr 26px}.bar{display:flex;align-items:center;gap:14px;padding:0 14px;border-bottom:1px solid var(--line);background:#0d120f}.brand{font:800 13px sans-serif;letter-spacing:.18em}.brand b{display:inline-grid;place-items:center;width:24px;height:24px;margin-right:9px;background:var(--acid);color:#101808}.bar small,.status{color:var(--muted)}.grow{flex:1}.bar button{border:1px solid #789e3f;border-radius:2px;background:var(--acid);color:#111908;padding:8px 14px;font:800 10px inherit}.body{min-height:0;display:grid;grid-template-columns:230px minmax(0,1fr) 285px}.side{overflow:auto;background:var(--panel)}.left{border-right:1px solid var(--line)}.right{border-left:1px solid var(--line)}.section{padding:13px;border-bottom:1px solid var(--line)}h2{margin:0 0 10px;color:var(--muted);font-size:9px;letter-spacing:.16em;text-transform:uppercase}label{display:grid;gap:5px;margin-bottom:10px;color:#91a097;font-size:9px;text-transform:uppercase}select,input{width:100%;border:1px solid var(--line);border-radius:2px;background:#090d0b;color:#dce4de;padding:8px;font:10px inherit;outline:none}select:focus,input:focus{border-color:var(--acid)}input:disabled{opacity:.38}.workspace{min-width:0;min-height:0;display:grid;grid-template-rows:minmax(280px,1fr) 224px;background:#090d0b}.viewport{position:relative;min-width:0;min-height:0}.viewport canvas{display:block;width:100%;height:100%}.title{position:absolute;top:18px;left:18px;pointer-events:none;font:800 23px sans-serif}.title small{display:block;margin-top:6px;color:var(--acid);font:9px ui-monospace;letter-spacing:.14em}.tools{position:absolute;bottom:16px;left:50%;display:flex;transform:translateX(-50%);border:1px solid var(--line2);background:#090d0be8}.tools button,.preset button{border:0;background:none;color:var(--muted);padding:9px 15px;font:9px inherit;text-transform:uppercase}.tools button.on{background:#263328;color:var(--acid)}.tools button:disabled{opacity:.28;cursor:not-allowed}.release-note{position:absolute;top:18px;right:18px;padding:7px 10px;border:1px solid #8b493d;background:#2b1713d9;color:#ff9b86;font-size:9px;letter-spacing:.13em}.landmarks{display:grid;gap:5px}.landmark{display:grid;grid-template-columns:8px 1fr auto;gap:8px;align-items:center;width:100%;padding:8px;border:1px solid transparent;border-radius:2px;background:#0c110f;color:var(--muted);font:10px inherit;text-align:left}.landmark.on{border-color:#668c34;color:#dce4de}.landmark i{width:6px;height:6px;border-radius:50%;background:#71b9ff}.landmark.detected i{background:var(--acid)}.landmark small{font-size:8px;color:#69756f}.axis{display:grid;grid-template-columns:repeat(3,1fr);gap:6px}.axis label{margin:0}.preset{display:grid;grid-template-columns:repeat(4,1fr);margin-top:8px}.preset button{padding:7px 2px;border:1px solid var(--line)}.wide,.editor-bar button,.segmented button{border:1px solid var(--line2);border-radius:2px;background:#1a211d;color:#dce4de;padding:8px;font:10px inherit}.wide{width:100%}.secondary{color:var(--muted)!important;background:#0d1210!important}.segmented{display:grid;grid-template-columns:1fr 1fr;gap:3px}.segmented button.on{border-color:var(--acid);color:var(--acid);background:#243021}.segmented button.danger.on{border-color:var(--red);color:#ff9b86;background:#321a16}.hint{margin:8px 0 0;color:var(--muted);font-size:9px;line-height:1.5}.status{display:flex;justify-content:space-between;align-items:center;padding:0 12px;border-top:1px solid var(--line);font-size:9px}.status b{color:var(--acid)}.error{position:absolute;top:18px;right:18px;max-width:360px;padding:10px;border:1px solid #81452f;background:#321a12;color:#ffb08a;font-size:10px}.saved{color:var(--acid);font-size:9px}.dope-editor{min-width:0;border-top:1px solid #46504b;background:#101512;box-shadow:0 -12px 30px #0008}.editor-bar{height:44px;display:flex;align-items:center;gap:7px;padding:6px 9px;border-bottom:1px solid var(--line2);background:#171c19}.editor-bar select{width:180px;padding:7px}.editor-bar button{white-space:nowrap;padding:7px 10px}.editor-bar button:disabled{opacity:.3;cursor:not-allowed}.editor-bar .transport{width:34px;border-color:#738d47;color:var(--acid);font-size:13px}.timecode{min-width:90px;color:#e1e7e3;font-size:11px;letter-spacing:.08em}.timecode i{color:var(--muted);font-style:normal}.compact{display:flex}.sheet{position:relative;height:179px;display:grid;grid-template-columns:132px 1fr;grid-template-rows:27px repeat(4,38px);overflow:hidden;background:repeating-linear-gradient(90deg,transparent 0,transparent calc(10% - 1px),#252d29 calc(10% - 1px),#252d29 10%)}.ruler-label{grid-column:1;grid-row:1}.ruler{position:relative;grid-column:2;grid-row:1;border-bottom:1px solid var(--line2);background:#0c100e}.ruler span{position:absolute;bottom:5px;transform:translateX(-50%);color:#68736d;font-size:8px}.lane-row{display:contents}.lane-label{z-index:3;display:flex;align-items:center;gap:8px;padding-left:13px;border-right:1px solid var(--line2);border-bottom:1px solid #222a26;background:#151a17;color:#87938c;font-size:9px;letter-spacing:.1em}.lane-label i{width:6px;height:6px;border:1px solid var(--cyan);transform:rotate(45deg)}.lane-track{position:relative;border-bottom:1px solid #222a26;background:#0d120f66}.key-diamond{position:absolute;z-index:4;top:14px;width:10px;height:10px;padding:0;transform:translate(-50%,-50%) rotate(45deg);border:1px solid #e2ffae;background:var(--acid);box-shadow:0 0 0 2px #0a0e0c}.attachment-lane .lane-label{grid-column:1;grid-row:5}.event-clip{align-self:center;grid-column:2;grid-row:5;height:23px;margin:0 8px;padding:6px 10px;border-left:3px solid var(--cyan);background:#17312d;color:#8ce7df;font-size:9px;letter-spacing:.08em}.event-clip.released{border-left-color:var(--red);background:#321a16;color:#ff9b86}.playhead{position:absolute;z-index:6;top:0;bottom:0;width:1px;background:#f3f7f4;pointer-events:none}.playhead b{position:absolute;top:0;left:-4px;width:9px;height:9px;background:#f3f7f4;clip-path:polygon(0 0,100% 0,50% 100%)}.scrubber{position:absolute;z-index:7;top:0;right:0;bottom:0;left:132px;width:calc(100% - 132px);height:100%;margin:0;padding:0;opacity:0;cursor:ew-resize}.ruler{grid-column:2;grid-row:1}.lane-row:nth-of-type(1) .lane-label,.lane-row:nth-of-type(1) .lane-track{grid-row:2}.lane-row:nth-of-type(2) .lane-label,.lane-row:nth-of-type(2) .lane-track{grid-row:3}.lane-row:nth-of-type(3) .lane-label,.lane-row:nth-of-type(3) .lane-track{grid-row:4}.lane-row .lane-label{grid-column:1}.lane-row .lane-track{grid-column:2}@media(max-width:980px){.body{grid-template-columns:210px 1fr}.right{display:none}.workspace{grid-template-rows:minmax(260px,1fr) 224px}.compact{display:none}}
|
|
10
|
-
`;
|
|
11
|
-
function escapeHtml(value) {
|
|
12
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll('"', """);
|
|
13
|
-
}
|
|
14
|
-
function poseLabPage(projectName) {
|
|
15
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Pose Lab \u2014 ${escapeHtml(projectName)}</title><style>${CSS}</style></head><body><div id="root" data-project="${escapeHtml(projectName)}"></div><script type="module" src="/app.js"></script></body></html>`;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// src/server.ts
|
|
19
|
-
var CLIENT = new URL("./client/app.js", import.meta.url);
|
|
20
|
-
var MAX_RECIPE_BYTES = 256e3;
|
|
21
|
-
function parseArgs(argv) {
|
|
22
|
-
const values = /* @__PURE__ */ new Map();
|
|
23
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
24
|
-
const flag = argv[index];
|
|
25
|
-
if (flag !== "--port" && flag !== "--project") throw new Error(`Unknown option: ${flag}`);
|
|
26
|
-
const value = argv[index + 1];
|
|
27
|
-
if (value === void 0) throw new Error(`${flag} requires a value.`);
|
|
28
|
-
values.set(flag, value);
|
|
29
|
-
index += 1;
|
|
30
|
-
}
|
|
31
|
-
const portText = values.get("--port");
|
|
32
|
-
const port = portText === void 0 ? void 0 : Number(portText);
|
|
33
|
-
if (port !== void 0 && (!Number.isInteger(port) || port < 1 || port > 65535))
|
|
34
|
-
throw new Error("--port must be an integer from 1 to 65535.");
|
|
35
|
-
return {
|
|
36
|
-
...port === void 0 ? {} : { port },
|
|
37
|
-
...values.get("--project") === void 0 ? {} : { project: values.get("--project") }
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
function projectFile(root, relative) {
|
|
41
|
-
if (relative.length === 0 || path.isAbsolute(relative) || relative.split(/[\\/]/u).some((part) => part.length === 0 || part.startsWith(".")))
|
|
42
|
-
return void 0;
|
|
43
|
-
const resolvedRoot = path.resolve(root);
|
|
44
|
-
const resolved = path.resolve(resolvedRoot, relative);
|
|
45
|
-
return resolved.startsWith(`${resolvedRoot}${path.sep}`) ? resolved : void 0;
|
|
46
|
-
}
|
|
47
|
-
async function modelsIn(project) {
|
|
48
|
-
const models = [];
|
|
49
|
-
const ignored = /* @__PURE__ */ new Set([".git", "build", "dist", "node_modules"]);
|
|
50
|
-
const visit = async (directory, relative, depth) => {
|
|
51
|
-
if (depth > 6 || models.length >= 2e3) return;
|
|
52
|
-
const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
53
|
-
for (const entry of entries) {
|
|
54
|
-
if (entry.name.startsWith(".") || ignored.has(entry.name)) continue;
|
|
55
|
-
const child = relative.length === 0 ? entry.name : `${relative}/${entry.name}`;
|
|
56
|
-
if (entry.isDirectory()) await visit(path.join(directory, entry.name), child, depth + 1);
|
|
57
|
-
else if (entry.name.toLowerCase().endsWith(".glb")) models.push(child);
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
await visit(project, "", 0);
|
|
61
|
-
return models.sort((left, right) => left.localeCompare(right));
|
|
62
|
-
}
|
|
63
|
-
async function readJson(request) {
|
|
64
|
-
const chunks = [];
|
|
65
|
-
let size = 0;
|
|
66
|
-
for await (const chunk of request) {
|
|
67
|
-
const value = Buffer.from(chunk);
|
|
68
|
-
size += value.length;
|
|
69
|
-
if (size > MAX_RECIPE_BYTES) throw new Error("Request exceeds 256 KB.");
|
|
70
|
-
chunks.push(value);
|
|
71
|
-
}
|
|
72
|
-
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
73
|
-
}
|
|
74
|
-
function send(response, status, value, type) {
|
|
75
|
-
response.writeHead(status, {
|
|
76
|
-
"cache-control": "no-store",
|
|
77
|
-
"content-type": type,
|
|
78
|
-
"x-content-type-options": "nosniff"
|
|
79
|
-
});
|
|
80
|
-
response.end(value);
|
|
81
|
-
}
|
|
82
|
-
function json(response, status, value) {
|
|
83
|
-
send(response, status, JSON.stringify(value), "application/json; charset=utf-8");
|
|
84
|
-
}
|
|
85
|
-
async function startPoseLab(options = {}) {
|
|
86
|
-
const project = path.resolve(options.project ?? process.cwd());
|
|
87
|
-
const port = options.port ?? 4192;
|
|
88
|
-
await readdir(project).catch(() => {
|
|
89
|
-
throw new Error(`Project directory does not exist: ${project}`);
|
|
90
|
-
});
|
|
91
|
-
const server = createServer(async (request, response) => {
|
|
92
|
-
try {
|
|
93
|
-
const url = new URL(request.url ?? "/", "http://pose-lab");
|
|
94
|
-
if (request.method === "GET" && (url.pathname === "/" || url.pathname === "/pose"))
|
|
95
|
-
return send(response, 200, poseLabPage(path.basename(project)), "text/html; charset=utf-8");
|
|
96
|
-
if (request.method === "GET" && url.pathname === "/app.js")
|
|
97
|
-
return send(response, 200, await readFile(CLIENT), "text/javascript; charset=utf-8");
|
|
98
|
-
if (request.method === "GET" && url.pathname === "/favicon.ico") {
|
|
99
|
-
response.writeHead(204);
|
|
100
|
-
return response.end();
|
|
101
|
-
}
|
|
102
|
-
if (request.method === "GET" && url.pathname === "/api/models")
|
|
103
|
-
return json(response, 200, { models: await modelsIn(project) });
|
|
104
|
-
if (request.method === "GET" && url.pathname === "/api/model") {
|
|
105
|
-
const relative = url.searchParams.get("path") ?? "";
|
|
106
|
-
const file = relative.endsWith(".glb") ? projectFile(project, relative) : void 0;
|
|
107
|
-
if (file === void 0) return json(response, 404, { error: "GLB not found." });
|
|
108
|
-
const model = await readFile(file).catch(() => void 0);
|
|
109
|
-
return model === void 0 ? json(response, 404, { error: "GLB not found." }) : send(response, 200, model, "model/gltf-binary");
|
|
110
|
-
}
|
|
111
|
-
if (url.pathname === "/api/recipe") {
|
|
112
|
-
const relative = url.searchParams.get("path") ?? "";
|
|
113
|
-
const file = relative.endsWith(".json") ? projectFile(project, relative) : void 0;
|
|
114
|
-
if (file === void 0) return json(response, 404, { error: "Recipe not found." });
|
|
115
|
-
if (request.method === "GET") {
|
|
116
|
-
const content = await readFile(file, "utf8").catch(() => void 0);
|
|
117
|
-
if (content === void 0) return json(response, 404, { error: "Recipe not found." });
|
|
118
|
-
return json(response, 200, { recipe: JSON.parse(content), path: relative });
|
|
119
|
-
}
|
|
120
|
-
if (request.method === "PUT") {
|
|
121
|
-
const body = await readJson(request);
|
|
122
|
-
if (typeof body.recipe !== "object" || body.recipe === null)
|
|
123
|
-
return json(response, 400, { error: "Body must contain a recipe object." });
|
|
124
|
-
await mkdir(path.dirname(file), { recursive: true });
|
|
125
|
-
await writeFile(file, `${JSON.stringify(body.recipe, null, 2)}
|
|
126
|
-
`, "utf8");
|
|
127
|
-
return json(response, 200, { path: relative, saved: true });
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return json(response, 404, { error: "Not found." });
|
|
131
|
-
} catch (error) {
|
|
132
|
-
return json(response, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
await new Promise((resolve, reject) => {
|
|
136
|
-
server.once("error", reject);
|
|
137
|
-
server.listen(port, "127.0.0.1", resolve);
|
|
138
|
-
});
|
|
139
|
-
const address = server.address();
|
|
140
|
-
return {
|
|
141
|
-
port: typeof address === "object" && address !== null ? address.port : port,
|
|
142
|
-
close: () => new Promise(
|
|
143
|
-
(resolve, reject) => server.close((error) => error ? reject(error) : resolve())
|
|
144
|
-
)
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
function invokedAsCli() {
|
|
148
|
-
const entry = process.argv[1];
|
|
149
|
-
return entry !== void 0 && path.resolve(entry) === path.resolve(fileURLToPath(import.meta.url));
|
|
150
|
-
}
|
|
151
|
-
if (invokedAsCli()) {
|
|
152
|
-
startPoseLab(parseArgs(process.argv.slice(2))).then(({ port }) => process.stdout.write(`Pose Lab: http://127.0.0.1:${port}/pose
|
|
153
|
-
`)).catch((error) => {
|
|
154
|
-
process.stderr.write(`${error instanceof Error ? error.message : String(error)}
|
|
155
|
-
`);
|
|
156
|
-
process.exitCode = 1;
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export { parseArgs, startPoseLab };
|