@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.
Files changed (58) hide show
  1. package/.env.example +14 -0
  2. package/LICENSE +21 -0
  3. package/README.md +108 -0
  4. package/dist/constants.d.ts +123 -0
  5. package/dist/constants.js +169 -0
  6. package/dist/index.d.ts +8 -0
  7. package/dist/index.js +130 -0
  8. package/dist/instructions.d.ts +6 -0
  9. package/dist/instructions.js +90 -0
  10. package/dist/schemas/balance.d.ts +11 -0
  11. package/dist/schemas/balance.js +8 -0
  12. package/dist/schemas/common.d.ts +38 -0
  13. package/dist/schemas/common.js +52 -0
  14. package/dist/schemas/generation.d.ts +219 -0
  15. package/dist/schemas/generation.js +217 -0
  16. package/dist/schemas/image.d.ts +55 -0
  17. package/dist/schemas/image.js +46 -0
  18. package/dist/schemas/output.d.ts +75 -0
  19. package/dist/schemas/output.js +41 -0
  20. package/dist/schemas/postprocessing.d.ts +135 -0
  21. package/dist/schemas/postprocessing.js +123 -0
  22. package/dist/schemas/printing.d.ts +63 -0
  23. package/dist/schemas/printing.js +54 -0
  24. package/dist/schemas/tasks.d.ts +123 -0
  25. package/dist/schemas/tasks.js +85 -0
  26. package/dist/services/error-handler.d.ts +32 -0
  27. package/dist/services/error-handler.js +141 -0
  28. package/dist/services/file-utils.d.ts +15 -0
  29. package/dist/services/file-utils.js +55 -0
  30. package/dist/services/meshy-client.d.ts +54 -0
  31. package/dist/services/meshy-client.js +172 -0
  32. package/dist/services/output-manager.d.ts +52 -0
  33. package/dist/services/output-manager.js +284 -0
  34. package/dist/tools/balance.d.ts +9 -0
  35. package/dist/tools/balance.js +61 -0
  36. package/dist/tools/generation.d.ts +9 -0
  37. package/dist/tools/generation.js +419 -0
  38. package/dist/tools/image.d.ts +9 -0
  39. package/dist/tools/image.js +154 -0
  40. package/dist/tools/postprocessing.d.ts +9 -0
  41. package/dist/tools/postprocessing.js +405 -0
  42. package/dist/tools/printing.d.ts +9 -0
  43. package/dist/tools/printing.js +338 -0
  44. package/dist/tools/tasks.d.ts +9 -0
  45. package/dist/tools/tasks.js +1074 -0
  46. package/dist/tools/workspace.d.ts +9 -0
  47. package/dist/tools/workspace.js +161 -0
  48. package/dist/types.d.ts +261 -0
  49. package/dist/types.js +4 -0
  50. package/dist/utils/endpoints.d.ts +16 -0
  51. package/dist/utils/endpoints.js +38 -0
  52. package/dist/utils/request-builder.d.ts +15 -0
  53. package/dist/utils/request-builder.js +24 -0
  54. package/dist/utils/response-formatter.d.ts +27 -0
  55. package/dist/utils/response-formatter.js +37 -0
  56. package/dist/utils/slicer-detector.d.ts +29 -0
  57. package/dist/utils/slicer-detector.js +237 -0
  58. 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;