@meshy-ai/meshy-mcp-server 0.2.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/.env.example +14 -0
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/constants.d.ts +123 -0
- package/dist/constants.js +169 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +130 -0
- package/dist/instructions.d.ts +6 -0
- package/dist/instructions.js +90 -0
- package/dist/schemas/balance.d.ts +11 -0
- package/dist/schemas/balance.js +8 -0
- package/dist/schemas/common.d.ts +38 -0
- package/dist/schemas/common.js +52 -0
- package/dist/schemas/generation.d.ts +219 -0
- package/dist/schemas/generation.js +217 -0
- package/dist/schemas/image.d.ts +55 -0
- package/dist/schemas/image.js +46 -0
- package/dist/schemas/output.d.ts +75 -0
- package/dist/schemas/output.js +41 -0
- package/dist/schemas/postprocessing.d.ts +135 -0
- package/dist/schemas/postprocessing.js +123 -0
- package/dist/schemas/printing.d.ts +63 -0
- package/dist/schemas/printing.js +54 -0
- package/dist/schemas/tasks.d.ts +123 -0
- package/dist/schemas/tasks.js +85 -0
- package/dist/services/error-handler.d.ts +32 -0
- package/dist/services/error-handler.js +141 -0
- package/dist/services/file-utils.d.ts +15 -0
- package/dist/services/file-utils.js +55 -0
- package/dist/services/meshy-client.d.ts +54 -0
- package/dist/services/meshy-client.js +172 -0
- package/dist/services/output-manager.d.ts +52 -0
- package/dist/services/output-manager.js +284 -0
- package/dist/tools/balance.d.ts +9 -0
- package/dist/tools/balance.js +61 -0
- package/dist/tools/generation.d.ts +9 -0
- package/dist/tools/generation.js +419 -0
- package/dist/tools/image.d.ts +9 -0
- package/dist/tools/image.js +154 -0
- package/dist/tools/postprocessing.d.ts +9 -0
- package/dist/tools/postprocessing.js +405 -0
- package/dist/tools/printing.d.ts +9 -0
- package/dist/tools/printing.js +338 -0
- package/dist/tools/tasks.d.ts +9 -0
- package/dist/tools/tasks.js +1074 -0
- package/dist/tools/workspace.d.ts +9 -0
- package/dist/tools/workspace.js +161 -0
- package/dist/types.d.ts +261 -0
- package/dist/types.js +4 -0
- package/dist/utils/endpoints.d.ts +16 -0
- package/dist/utils/endpoints.js +38 -0
- package/dist/utils/request-builder.d.ts +15 -0
- package/dist/utils/request-builder.js +24 -0
- package/dist/utils/response-formatter.d.ts +27 -0
- package/dist/utils/response-formatter.js +37 -0
- package/dist/utils/slicer-detector.d.ts +29 -0
- package/dist/utils/slicer-detector.js +237 -0
- package/package.json +64 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output directory manager for Meshy generation tasks.
|
|
3
|
+
*
|
|
4
|
+
* Organizes all downloaded files under {cwd}/meshy_output/ with:
|
|
5
|
+
* - Per-project folders: {YYYYMMDD_HHmmss}_{prompt_slug}_{task_id_prefix}/
|
|
6
|
+
* - Auto-downloaded thumbnails
|
|
7
|
+
* - Per-project metadata.json tracking task chains
|
|
8
|
+
* - Global history.json index
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from "fs";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
import axios from "axios";
|
|
13
|
+
const OUTPUT_DIR_NAME = "meshy_output";
|
|
14
|
+
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
15
|
+
function getOutputRoot() {
|
|
16
|
+
return path.join(process.cwd(), OUTPUT_DIR_NAME);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Sanitize a prompt into a filesystem-safe slug (max 30 chars).
|
|
20
|
+
*/
|
|
21
|
+
function slugify(text) {
|
|
22
|
+
return text
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.replace(/[^a-z0-9\u4e00-\u9fff]+/g, "-") // keep CJK chars
|
|
25
|
+
.replace(/^-+|-+$/g, "")
|
|
26
|
+
.slice(0, 30)
|
|
27
|
+
.replace(/-+$/, "");
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Format a date as YYYYMMDD_HHmmss in local timezone.
|
|
31
|
+
*/
|
|
32
|
+
function formatTimestamp(date) {
|
|
33
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
34
|
+
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}_${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Infer a human-readable stage name from task type and API response type field.
|
|
38
|
+
*/
|
|
39
|
+
export function inferStage(taskType, apiType) {
|
|
40
|
+
if (apiType) {
|
|
41
|
+
if (apiType.includes("preview"))
|
|
42
|
+
return "preview";
|
|
43
|
+
if (apiType.includes("refine"))
|
|
44
|
+
return "refined";
|
|
45
|
+
}
|
|
46
|
+
switch (taskType) {
|
|
47
|
+
case "text-to-3d": return "model";
|
|
48
|
+
case "image-to-3d": return "model";
|
|
49
|
+
case "multi-image-to-3d": return "model";
|
|
50
|
+
case "remesh": return "remeshed";
|
|
51
|
+
case "retexture": return "retextured";
|
|
52
|
+
case "rigging": return "rigged";
|
|
53
|
+
case "animation": return "animated";
|
|
54
|
+
case "multi-color-print": return "multicolor";
|
|
55
|
+
default: return "model";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// ─── Core Functions ──────────────────────────────────────────────────
|
|
59
|
+
/**
|
|
60
|
+
* Ensure the output root directory exists.
|
|
61
|
+
*/
|
|
62
|
+
function ensureOutputRoot() {
|
|
63
|
+
const root = getOutputRoot();
|
|
64
|
+
if (!fs.existsSync(root)) {
|
|
65
|
+
fs.mkdirSync(root, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
return root;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Find an existing project folder by root_task_id (for chained tasks like refine/rig/animate).
|
|
71
|
+
*/
|
|
72
|
+
function findProjectByRootTask(rootTaskId) {
|
|
73
|
+
const root = getOutputRoot();
|
|
74
|
+
if (!fs.existsSync(root))
|
|
75
|
+
return null;
|
|
76
|
+
const historyPath = path.join(root, "history.json");
|
|
77
|
+
if (!fs.existsSync(historyPath))
|
|
78
|
+
return null;
|
|
79
|
+
try {
|
|
80
|
+
const history = JSON.parse(fs.readFileSync(historyPath, "utf-8"));
|
|
81
|
+
const entry = history.projects.find(p => p.root_task_id === rootTaskId);
|
|
82
|
+
if (entry) {
|
|
83
|
+
const fullPath = path.join(root, entry.folder);
|
|
84
|
+
if (fs.existsSync(fullPath))
|
|
85
|
+
return fullPath;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Corrupted history, ignore
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Find an existing project folder that contains a specific task_id.
|
|
95
|
+
*/
|
|
96
|
+
function findProjectByTaskId(taskId) {
|
|
97
|
+
const root = getOutputRoot();
|
|
98
|
+
if (!fs.existsSync(root))
|
|
99
|
+
return null;
|
|
100
|
+
const historyPath = path.join(root, "history.json");
|
|
101
|
+
if (!fs.existsSync(historyPath))
|
|
102
|
+
return null;
|
|
103
|
+
try {
|
|
104
|
+
const history = JSON.parse(fs.readFileSync(historyPath, "utf-8"));
|
|
105
|
+
for (const entry of history.projects) {
|
|
106
|
+
const metaPath = path.join(root, entry.folder, "metadata.json");
|
|
107
|
+
if (!fs.existsSync(metaPath))
|
|
108
|
+
continue;
|
|
109
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
110
|
+
if (meta.tasks.some(t => t.task_id === taskId)) {
|
|
111
|
+
return path.join(root, entry.folder);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Ignore
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Resolve (or create) the project directory for a task.
|
|
122
|
+
*
|
|
123
|
+
* For chained tasks (refine → preview, rig → source), pass parentTaskId
|
|
124
|
+
* to place the output in the same project folder as the parent.
|
|
125
|
+
*/
|
|
126
|
+
export function resolveProjectDir(taskId, taskType, prompt, parentTaskId, createdAt) {
|
|
127
|
+
const root = ensureOutputRoot();
|
|
128
|
+
// 1. Check if this task already has a project folder
|
|
129
|
+
const existing = findProjectByTaskId(taskId);
|
|
130
|
+
if (existing)
|
|
131
|
+
return existing;
|
|
132
|
+
// 2. Check if parent task has a project folder (for chained tasks)
|
|
133
|
+
if (parentTaskId) {
|
|
134
|
+
const parentDir = findProjectByTaskId(parentTaskId);
|
|
135
|
+
if (parentDir)
|
|
136
|
+
return parentDir;
|
|
137
|
+
const parentRoot = findProjectByRootTask(parentTaskId);
|
|
138
|
+
if (parentRoot)
|
|
139
|
+
return parentRoot;
|
|
140
|
+
}
|
|
141
|
+
// 3. Create new project folder: {YYYYMMDD_HHmmss}_{prompt_slug}_{task_id_prefix}
|
|
142
|
+
const date = createdAt ? new Date(typeof createdAt === "number" ? createdAt : createdAt) : new Date();
|
|
143
|
+
const timestamp = formatTimestamp(date);
|
|
144
|
+
const slug = prompt ? slugify(prompt) : taskType;
|
|
145
|
+
const idPrefix = taskId.slice(0, 8);
|
|
146
|
+
const folderName = `${timestamp}_${slug}_${idPrefix}`;
|
|
147
|
+
const projectDir = path.join(root, folderName);
|
|
148
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
149
|
+
// Initialize metadata.json
|
|
150
|
+
const metadata = {
|
|
151
|
+
project_name: prompt || taskType,
|
|
152
|
+
folder: folderName,
|
|
153
|
+
root_task_id: taskId,
|
|
154
|
+
created_at: date.toISOString(),
|
|
155
|
+
updated_at: date.toISOString(),
|
|
156
|
+
tasks: []
|
|
157
|
+
};
|
|
158
|
+
fs.writeFileSync(path.join(projectDir, "metadata.json"), JSON.stringify(metadata, null, 2));
|
|
159
|
+
// Update global history
|
|
160
|
+
updateHistory(folderName, {
|
|
161
|
+
folder: folderName,
|
|
162
|
+
prompt: prompt || "",
|
|
163
|
+
task_type: taskType,
|
|
164
|
+
root_task_id: taskId,
|
|
165
|
+
created_at: date.toISOString(),
|
|
166
|
+
updated_at: date.toISOString(),
|
|
167
|
+
task_count: 0
|
|
168
|
+
});
|
|
169
|
+
return projectDir;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Generate the file path for a model download within a project directory.
|
|
173
|
+
* Returns: /path/to/project/stage.ext (e.g., preview.glb, refined.glb)
|
|
174
|
+
*/
|
|
175
|
+
export function getFilePath(projectDir, stage, format) {
|
|
176
|
+
return path.join(projectDir, `${stage}.${format}`);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Generate texture file path within a project directory.
|
|
180
|
+
* Returns: /path/to/project/stage_texType.ext (e.g., refined_base_color.png)
|
|
181
|
+
*/
|
|
182
|
+
export function getTextureFilePath(projectDir, stage, textureType, url) {
|
|
183
|
+
const ext = url.includes(".png") ? ".png" : ".jpg";
|
|
184
|
+
return path.join(projectDir, `${stage}_${textureType}${ext}`);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Record a completed task into the project's metadata.json.
|
|
188
|
+
*/
|
|
189
|
+
export function recordTask(projectDir, record) {
|
|
190
|
+
const metaPath = path.join(projectDir, "metadata.json");
|
|
191
|
+
let metadata;
|
|
192
|
+
if (fs.existsSync(metaPath)) {
|
|
193
|
+
metadata = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
metadata = {
|
|
197
|
+
project_name: record.prompt || record.task_type,
|
|
198
|
+
folder: path.basename(projectDir),
|
|
199
|
+
root_task_id: record.task_id,
|
|
200
|
+
created_at: new Date().toISOString(),
|
|
201
|
+
updated_at: new Date().toISOString(),
|
|
202
|
+
tasks: []
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
// Avoid duplicate records
|
|
206
|
+
if (!metadata.tasks.some(t => t.task_id === record.task_id && t.stage === record.stage)) {
|
|
207
|
+
metadata.tasks.push(record);
|
|
208
|
+
}
|
|
209
|
+
metadata.updated_at = new Date().toISOString();
|
|
210
|
+
fs.writeFileSync(metaPath, JSON.stringify(metadata, null, 2));
|
|
211
|
+
// Update history task count
|
|
212
|
+
const folderName = path.basename(projectDir);
|
|
213
|
+
const root = getOutputRoot();
|
|
214
|
+
const historyPath = path.join(root, "history.json");
|
|
215
|
+
if (fs.existsSync(historyPath)) {
|
|
216
|
+
try {
|
|
217
|
+
const history = JSON.parse(fs.readFileSync(historyPath, "utf-8"));
|
|
218
|
+
const entry = history.projects.find(p => p.folder === folderName);
|
|
219
|
+
if (entry) {
|
|
220
|
+
entry.task_count = metadata.tasks.length;
|
|
221
|
+
entry.updated_at = metadata.updated_at;
|
|
222
|
+
fs.writeFileSync(historyPath, JSON.stringify(history, null, 2));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// Ignore
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Download and save thumbnail to the project directory.
|
|
232
|
+
* Silently skips on failure.
|
|
233
|
+
*/
|
|
234
|
+
export async function saveThumbnail(projectDir, thumbnailUrl) {
|
|
235
|
+
const thumbPath = path.join(projectDir, "thumbnail.png");
|
|
236
|
+
// Skip if already downloaded
|
|
237
|
+
if (fs.existsSync(thumbPath))
|
|
238
|
+
return thumbPath;
|
|
239
|
+
try {
|
|
240
|
+
const response = await axios.get(thumbnailUrl, {
|
|
241
|
+
responseType: "arraybuffer",
|
|
242
|
+
timeout: 15000
|
|
243
|
+
});
|
|
244
|
+
fs.writeFileSync(thumbPath, Buffer.from(response.data));
|
|
245
|
+
return thumbPath;
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Update (or create) the global history.json index.
|
|
253
|
+
*/
|
|
254
|
+
function updateHistory(folderName, entry) {
|
|
255
|
+
const root = ensureOutputRoot();
|
|
256
|
+
const historyPath = path.join(root, "history.json");
|
|
257
|
+
let history;
|
|
258
|
+
if (fs.existsSync(historyPath)) {
|
|
259
|
+
try {
|
|
260
|
+
history = JSON.parse(fs.readFileSync(historyPath, "utf-8"));
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
history = { version: 1, projects: [] };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
history = { version: 1, projects: [] };
|
|
268
|
+
}
|
|
269
|
+
// Update existing or add new
|
|
270
|
+
const idx = history.projects.findIndex(p => p.folder === folderName);
|
|
271
|
+
if (idx >= 0) {
|
|
272
|
+
history.projects[idx] = { ...history.projects[idx], ...entry };
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
history.projects.push(entry);
|
|
276
|
+
}
|
|
277
|
+
fs.writeFileSync(historyPath, JSON.stringify(history, null, 2));
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Get the output root path (for display purposes).
|
|
281
|
+
*/
|
|
282
|
+
export function getOutputRootPath() {
|
|
283
|
+
return getOutputRoot();
|
|
284
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Balance tool — check Meshy account credit balance
|
|
3
|
+
*/
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { MeshyClient } from "../services/meshy-client.js";
|
|
6
|
+
/**
|
|
7
|
+
* Register the balance tool with the MCP server
|
|
8
|
+
*/
|
|
9
|
+
export declare function registerBalanceTool(server: McpServer, client: MeshyClient): void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Balance tool — check Meshy account credit balance
|
|
3
|
+
*/
|
|
4
|
+
import { handleMeshyError } from "../services/error-handler.js";
|
|
5
|
+
import { CheckBalanceInputSchema } from "../schemas/balance.js";
|
|
6
|
+
import { BalanceOutputSchema } from "../schemas/output.js";
|
|
7
|
+
import { ResponseFormat } from "../constants.js";
|
|
8
|
+
/**
|
|
9
|
+
* Register the balance tool with the MCP server
|
|
10
|
+
*/
|
|
11
|
+
export function registerBalanceTool(server, client) {
|
|
12
|
+
server.registerTool("meshy_check_balance", {
|
|
13
|
+
title: "Check Credit Balance",
|
|
14
|
+
description: `Check your Meshy account credit balance.
|
|
15
|
+
|
|
16
|
+
Returns the current number of credits available in your account.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
- response_format (enum): Output format - "markdown" or "json" (default: "markdown")
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
{ "balance": 150 }
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
- Check balance: {}
|
|
26
|
+
- JSON format: { response_format: "json" }`,
|
|
27
|
+
inputSchema: CheckBalanceInputSchema,
|
|
28
|
+
outputSchema: BalanceOutputSchema,
|
|
29
|
+
annotations: {
|
|
30
|
+
readOnlyHint: true,
|
|
31
|
+
destructiveHint: false,
|
|
32
|
+
idempotentHint: true,
|
|
33
|
+
openWorldHint: true
|
|
34
|
+
}
|
|
35
|
+
}, async (params) => {
|
|
36
|
+
try {
|
|
37
|
+
const data = await client.get("/openapi/v1/balance");
|
|
38
|
+
const output = { balance: data.balance };
|
|
39
|
+
let textContent;
|
|
40
|
+
if (params.response_format === ResponseFormat.MARKDOWN) {
|
|
41
|
+
textContent = `# Meshy Credit Balance\n\n**Balance**: ${data.balance} credits`;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
textContent = JSON.stringify(output, null, 2);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: "text", text: textContent }],
|
|
48
|
+
structuredContent: output
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return {
|
|
53
|
+
isError: true,
|
|
54
|
+
content: [{
|
|
55
|
+
type: "text",
|
|
56
|
+
text: handleMeshyError(error)
|
|
57
|
+
}]
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generation tools (text-to-3d, image-to-3d)
|
|
3
|
+
*/
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { MeshyClient } from "../services/meshy-client.js";
|
|
6
|
+
/**
|
|
7
|
+
* Register generation tools with the MCP server
|
|
8
|
+
*/
|
|
9
|
+
export declare function registerGenerationTools(server: McpServer, client: MeshyClient): void;
|