@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,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schemas for task management tools
|
|
3
|
+
*/
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { TaskStatus, TaskPhase, TaskType, POLL_MAX_TIMEOUT } from "../constants.js";
|
|
6
|
+
import { ResponseFormatSchema, PaginationSchema, TaskIdSchema, TaskTypeSchema } from "./common.js";
|
|
7
|
+
/**
|
|
8
|
+
* Get task status input schema (also supports wait mode)
|
|
9
|
+
*/
|
|
10
|
+
export const GetTaskStatusInputSchema = z.object({
|
|
11
|
+
task_id: TaskIdSchema,
|
|
12
|
+
task_type: TaskTypeSchema,
|
|
13
|
+
wait: z.boolean()
|
|
14
|
+
.default(true)
|
|
15
|
+
.describe("If true (default), auto-poll until task completes. If false, return current status immediately."),
|
|
16
|
+
timeout_seconds: z.number()
|
|
17
|
+
.int()
|
|
18
|
+
.min(10, "Timeout must be at least 10 seconds")
|
|
19
|
+
.max(POLL_MAX_TIMEOUT / 1000, `Timeout cannot exceed ${POLL_MAX_TIMEOUT / 1000} seconds`)
|
|
20
|
+
.default(300)
|
|
21
|
+
.describe("Maximum wait time in seconds when wait=true (default: 300, max: 300)"),
|
|
22
|
+
response_format: ResponseFormatSchema
|
|
23
|
+
}).strict();
|
|
24
|
+
/**
|
|
25
|
+
* List tasks input schema
|
|
26
|
+
*/
|
|
27
|
+
export const ListTasksInputSchema = z.object({
|
|
28
|
+
task_type: z.nativeEnum(TaskType)
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("Filter by task type. If omitted, queries ALL task types (text-to-3d, image-to-3d, multi-image-to-3d, remesh, retexture, text-to-image, image-to-image) and merges results. Note: rigging and animation do not have list endpoints."),
|
|
31
|
+
sort_by: z.enum(["+created_at", "-created_at"])
|
|
32
|
+
.default("-created_at")
|
|
33
|
+
.describe("Sort order by creation time. '-created_at' = newest first (default), '+created_at' = oldest first"),
|
|
34
|
+
status: z.nativeEnum(TaskStatus)
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("Filter by task status"),
|
|
37
|
+
phase: z.nativeEnum(TaskPhase)
|
|
38
|
+
.optional()
|
|
39
|
+
.describe("Filter by task phase"),
|
|
40
|
+
response_format: ResponseFormatSchema
|
|
41
|
+
}).merge(PaginationSchema).strict();
|
|
42
|
+
/**
|
|
43
|
+
* Cancel task input schema
|
|
44
|
+
*/
|
|
45
|
+
export const CancelTaskInputSchema = z.object({
|
|
46
|
+
task_id: TaskIdSchema,
|
|
47
|
+
task_type: TaskTypeSchema
|
|
48
|
+
}).strict();
|
|
49
|
+
/**
|
|
50
|
+
* Download model input schema
|
|
51
|
+
*/
|
|
52
|
+
export const DownloadModelInputSchema = z.object({
|
|
53
|
+
task_id: TaskIdSchema,
|
|
54
|
+
task_type: TaskTypeSchema,
|
|
55
|
+
format: z.enum(["glb", "fbx", "usdz", "stl", "obj", "3mf"])
|
|
56
|
+
.default("glb")
|
|
57
|
+
.describe("Model format to download. IMPORTANT: Ask the user which format they need before downloading. Recommendations: GLB (general viewing), OBJ (white model printing), 3MF (multicolor printing), FBX (game engines/animation), USDZ (AR/Apple). Do NOT download all formats."),
|
|
58
|
+
include_textures: z.boolean()
|
|
59
|
+
.default(true)
|
|
60
|
+
.describe("Include texture files in response"),
|
|
61
|
+
save_to: z.string()
|
|
62
|
+
.optional()
|
|
63
|
+
.describe("Override auto-save path with a custom ABSOLUTE path. If omitted, auto-saves to meshy_output/{timestamp}_{prompt}_{id}/. Example: /Users/me/models/chair.glb"),
|
|
64
|
+
parent_task_id: z.string()
|
|
65
|
+
.optional()
|
|
66
|
+
.describe("Parent task ID for chaining (e.g., preview_task_id for refine, input_task_id for rig). Places output in the same project folder as the parent."),
|
|
67
|
+
print_ready: z.boolean()
|
|
68
|
+
.optional()
|
|
69
|
+
.describe("If true and format is OBJ, auto-fix coordinates for 3D printing: rotates Y-up to Z-up, scales to target height, centers on XY, aligns bottom to Z=0. Default false."),
|
|
70
|
+
print_height_mm: z.number()
|
|
71
|
+
.optional()
|
|
72
|
+
.describe("Target height in mm when print_ready is true. Default 75. Adjust per user request (e.g. 'print at 15cm' → 150).")
|
|
73
|
+
}).strict();
|
|
74
|
+
/**
|
|
75
|
+
* List models input schema
|
|
76
|
+
*/
|
|
77
|
+
export const ListModelsInputSchema = z.object({
|
|
78
|
+
workspace_id: z.string()
|
|
79
|
+
.optional()
|
|
80
|
+
.describe("Workspace ID (uses default if omitted)"),
|
|
81
|
+
filter: z.enum(["all", "published", "private"])
|
|
82
|
+
.default("all")
|
|
83
|
+
.describe("Filter models by visibility"),
|
|
84
|
+
response_format: ResponseFormatSchema
|
|
85
|
+
}).merge(PaginationSchema).strict();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized error handling for Meshy API
|
|
3
|
+
*/
|
|
4
|
+
export interface MeshyError {
|
|
5
|
+
code: string;
|
|
6
|
+
message: string;
|
|
7
|
+
status?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Optional context for richer error messages
|
|
11
|
+
*/
|
|
12
|
+
export interface ErrorContext {
|
|
13
|
+
tool?: string;
|
|
14
|
+
taskId?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Handle Meshy API errors and convert to user-friendly messages.
|
|
18
|
+
* When context is provided, appends tool-specific recovery suggestions.
|
|
19
|
+
*/
|
|
20
|
+
export declare function handleMeshyError(error: unknown, context?: ErrorContext): string;
|
|
21
|
+
/**
|
|
22
|
+
* Check if error is a rate limit error
|
|
23
|
+
*/
|
|
24
|
+
export declare function isRateLimitError(error: unknown): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Check if error is a network error that should be retried
|
|
27
|
+
*/
|
|
28
|
+
export declare function isRetryableError(error: unknown): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Extract error message from various error formats
|
|
31
|
+
*/
|
|
32
|
+
export declare function extractErrorMessage(error: unknown): string;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized error handling for Meshy API
|
|
3
|
+
*/
|
|
4
|
+
import { AxiosError } from "axios";
|
|
5
|
+
import { ErrorCode } from "../constants.js";
|
|
6
|
+
/**
|
|
7
|
+
* Handle Meshy API errors and convert to user-friendly messages.
|
|
8
|
+
* When context is provided, appends tool-specific recovery suggestions.
|
|
9
|
+
*/
|
|
10
|
+
export function handleMeshyError(error, context) {
|
|
11
|
+
// Handle Axios errors
|
|
12
|
+
if (error instanceof AxiosError) {
|
|
13
|
+
if (error.response) {
|
|
14
|
+
const status = error.response.status;
|
|
15
|
+
const errorData = error.response.data;
|
|
16
|
+
const errorCode = errorData?.error_code || errorData?.code;
|
|
17
|
+
// Map specific error codes
|
|
18
|
+
switch (errorCode) {
|
|
19
|
+
case ErrorCode.INSUFFICIENT_CREDITS:
|
|
20
|
+
return "Error: Insufficient credits. Use `meshy_check_balance` to check your balance. Upgrade at https://meshy.ai/pricing";
|
|
21
|
+
case ErrorCode.TOO_MANY_PENDING_TASKS:
|
|
22
|
+
return "Error: Too many pending tasks. Please wait for current tasks to complete or cancel some tasks.";
|
|
23
|
+
case ErrorCode.INVALID_MODEL_NOT_SUPPORTED:
|
|
24
|
+
return "Error: Model format not supported. Supported formats: GLB, FBX, USDZ, 3MF";
|
|
25
|
+
case ErrorCode.INVALID_MODEL_INVALID_FORMAT:
|
|
26
|
+
return "Error: Model file is corrupted or invalid. Please try generating again.";
|
|
27
|
+
case ErrorCode.LIMIT_EXCEEDED:
|
|
28
|
+
return "Error: Rate limit exceeded. Please wait a moment before making more requests.";
|
|
29
|
+
case ErrorCode.FORBIDDEN:
|
|
30
|
+
return "Error: Permission denied. Please check your API key has the required permissions.";
|
|
31
|
+
case ErrorCode.NOT_FOUND:
|
|
32
|
+
return "Error: Resource not found. Please verify the task ID is correct.";
|
|
33
|
+
case ErrorCode.INTERNAL_ERROR:
|
|
34
|
+
return "Error: Meshy service error. Please try again later.";
|
|
35
|
+
}
|
|
36
|
+
// Handle HTTP status codes
|
|
37
|
+
switch (status) {
|
|
38
|
+
case 400:
|
|
39
|
+
return `Error: Invalid request. ${errorData?.message || "Please check your parameters."}`;
|
|
40
|
+
case 401:
|
|
41
|
+
return "Error: Authentication failed. Please check your MESHY_API_KEY is valid.";
|
|
42
|
+
case 403:
|
|
43
|
+
return "Error: Permission denied. Your API key may not have access to this resource.";
|
|
44
|
+
case 404:
|
|
45
|
+
return "Error: Resource not found. Please check the ID is correct.";
|
|
46
|
+
case 429:
|
|
47
|
+
return "Error: Rate limit exceeded. Please wait before making more requests.";
|
|
48
|
+
case 500:
|
|
49
|
+
case 502:
|
|
50
|
+
case 503:
|
|
51
|
+
return "Error: Meshy service is temporarily unavailable. Please try again later.";
|
|
52
|
+
default:
|
|
53
|
+
return `Error: API request failed with status ${status}. ${errorData?.message || ""}`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Handle network errors
|
|
57
|
+
if (error.code === "ECONNABORTED") {
|
|
58
|
+
return "Error: Request timed out. Please check your network connection and try again.";
|
|
59
|
+
}
|
|
60
|
+
if (error.code === "ENOTFOUND") {
|
|
61
|
+
return "Error: Cannot reach Meshy API. Please check your internet connection.";
|
|
62
|
+
}
|
|
63
|
+
if (error.code === "ECONNREFUSED") {
|
|
64
|
+
return "Error: Meshy API is unavailable. Please try again later.";
|
|
65
|
+
}
|
|
66
|
+
return `Error: Network error occurred. ${error.message}`;
|
|
67
|
+
}
|
|
68
|
+
// Handle generic errors
|
|
69
|
+
let baseMessage;
|
|
70
|
+
if (error instanceof Error) {
|
|
71
|
+
baseMessage = `Error: ${error.message}`;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
baseMessage = `Error: An unexpected error occurred. ${String(error)}`;
|
|
75
|
+
}
|
|
76
|
+
// Append context-aware suggestions if context is provided
|
|
77
|
+
return appendContextSuggestions(baseMessage, error, context);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Append context-specific recovery suggestions to error messages
|
|
81
|
+
*/
|
|
82
|
+
function appendContextSuggestions(message, error, context) {
|
|
83
|
+
if (!context?.tool)
|
|
84
|
+
return message;
|
|
85
|
+
const errorText = message.toLowerCase();
|
|
86
|
+
const tool = context.tool;
|
|
87
|
+
// Rig + face limit
|
|
88
|
+
if (tool === "meshy_rig" && (errorText.includes("face") || errorText.includes("300,000") || errorText.includes("300000") || errorText.includes("limit"))) {
|
|
89
|
+
return message + `\n\n**Fix**: The model exceeds the 300K face limit for rigging. Call \`meshy_remesh\` with target_polycount 100000 first, then rig the remeshed output.`;
|
|
90
|
+
}
|
|
91
|
+
// Image-to-3D + image errors
|
|
92
|
+
if ((tool === "meshy_image_to_3d" || tool === "meshy_multi_image_to_3d") && (errorText.includes("image") || errorText.includes("url"))) {
|
|
93
|
+
return message + `\n\n**Fix**: For local images, use \`file_path\` parameter (absolute path like "/Users/me/photo.jpg"). The server handles encoding automatically. Do NOT manually base64-encode.`;
|
|
94
|
+
}
|
|
95
|
+
// Multi-color + missing texture
|
|
96
|
+
if (tool === "meshy_process_multicolor" && (errorText.includes("texture") || errorText.includes("input") || errorText.includes("task"))) {
|
|
97
|
+
return message + `\n\n**Fix**: The input model must have textures. Run \`meshy_text_to_3d_refine\` or \`meshy_retexture\` first to add textures, then use the resulting task ID as input_task_id.`;
|
|
98
|
+
}
|
|
99
|
+
// Insufficient credits
|
|
100
|
+
if (errorText.includes("insufficient") || errorText.includes("credit")) {
|
|
101
|
+
return message + `\n\n**Fix**: Check your credit balance at https://meshy.ai/pricing. Current tool: ${tool}.`;
|
|
102
|
+
}
|
|
103
|
+
return message;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Check if error is a rate limit error
|
|
107
|
+
*/
|
|
108
|
+
export function isRateLimitError(error) {
|
|
109
|
+
if (error instanceof AxiosError) {
|
|
110
|
+
return error.response?.status === 429;
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Check if error is a network error that should be retried
|
|
116
|
+
*/
|
|
117
|
+
export function isRetryableError(error) {
|
|
118
|
+
if (error instanceof AxiosError) {
|
|
119
|
+
// Retry on network errors
|
|
120
|
+
if (!error.response) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
// Retry on 5xx server errors
|
|
124
|
+
const status = error.response.status;
|
|
125
|
+
return status >= 500 && status < 600;
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Extract error message from various error formats
|
|
131
|
+
*/
|
|
132
|
+
export function extractErrorMessage(error) {
|
|
133
|
+
if (error instanceof AxiosError && error.response?.data) {
|
|
134
|
+
const data = error.response.data;
|
|
135
|
+
return data.message || data.error || data.error_message || "Unknown error";
|
|
136
|
+
}
|
|
137
|
+
if (error instanceof Error) {
|
|
138
|
+
return error.message;
|
|
139
|
+
}
|
|
140
|
+
return String(error);
|
|
141
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for reading local image files and converting to base64 data URIs.
|
|
3
|
+
* Used by image-to-3d, multi-image-to-3d, and image-to-image tools
|
|
4
|
+
* so agents don't need to pass huge base64 strings through MCP arguments.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Read a local image file and return a base64 data URI.
|
|
8
|
+
* Throws descriptive errors for missing files, unsupported formats, or oversized files.
|
|
9
|
+
*/
|
|
10
|
+
export declare function fileToDataUri(filePath: string): Promise<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Resolve an image source: if file_path is provided, convert to data URI;
|
|
13
|
+
* otherwise return image_url as-is.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveImageSource(image_url?: string, file_path?: string): Promise<string>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for reading local image files and converting to base64 data URIs.
|
|
3
|
+
* Used by image-to-3d, multi-image-to-3d, and image-to-image tools
|
|
4
|
+
* so agents don't need to pass huge base64 strings through MCP arguments.
|
|
5
|
+
*/
|
|
6
|
+
import { readFile } from "fs/promises";
|
|
7
|
+
import { extname } from "path";
|
|
8
|
+
const MIME_TYPES = {
|
|
9
|
+
".jpg": "image/jpeg",
|
|
10
|
+
".jpeg": "image/jpeg",
|
|
11
|
+
".png": "image/png",
|
|
12
|
+
".webp": "image/webp",
|
|
13
|
+
};
|
|
14
|
+
const MAX_FILE_SIZE = 20 * 1024 * 1024; // 20MB
|
|
15
|
+
/**
|
|
16
|
+
* Read a local image file and return a base64 data URI.
|
|
17
|
+
* Throws descriptive errors for missing files, unsupported formats, or oversized files.
|
|
18
|
+
*/
|
|
19
|
+
export async function fileToDataUri(filePath) {
|
|
20
|
+
const ext = extname(filePath).toLowerCase();
|
|
21
|
+
const mime = MIME_TYPES[ext];
|
|
22
|
+
if (!mime) {
|
|
23
|
+
throw new Error(`Unsupported image format "${ext}". Supported: ${Object.keys(MIME_TYPES).join(", ")}`);
|
|
24
|
+
}
|
|
25
|
+
let buffer;
|
|
26
|
+
try {
|
|
27
|
+
buffer = await readFile(filePath);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
if (err.code === "ENOENT") {
|
|
31
|
+
throw new Error(`File not found: ${filePath}`);
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`Failed to read file ${filePath}: ${err.message}`);
|
|
34
|
+
}
|
|
35
|
+
if (buffer.length > MAX_FILE_SIZE) {
|
|
36
|
+
const sizeMB = (buffer.length / (1024 * 1024)).toFixed(1);
|
|
37
|
+
throw new Error(`File too large (${sizeMB} MB). Maximum is ${MAX_FILE_SIZE / (1024 * 1024)} MB.`);
|
|
38
|
+
}
|
|
39
|
+
const base64 = buffer.toString("base64");
|
|
40
|
+
return `data:${mime};base64,${base64}`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolve an image source: if file_path is provided, convert to data URI;
|
|
44
|
+
* otherwise return image_url as-is.
|
|
45
|
+
*/
|
|
46
|
+
export async function resolveImageSource(image_url, file_path) {
|
|
47
|
+
if (file_path) {
|
|
48
|
+
return fileToDataUri(file_path);
|
|
49
|
+
}
|
|
50
|
+
if (image_url) {
|
|
51
|
+
return image_url;
|
|
52
|
+
}
|
|
53
|
+
throw new Error("Either image_url or file_path must be provided. " +
|
|
54
|
+
"Use file_path for local files, image_url for public URLs.");
|
|
55
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meshy API client with authentication and error handling
|
|
3
|
+
*/
|
|
4
|
+
import { GetTaskResponse } from "../types.js";
|
|
5
|
+
export declare class MeshyClient {
|
|
6
|
+
private client;
|
|
7
|
+
private apiKey;
|
|
8
|
+
constructor(apiKey: string);
|
|
9
|
+
/**
|
|
10
|
+
* Make a GET request with retry logic
|
|
11
|
+
*/
|
|
12
|
+
get<T>(endpoint: string, params?: Record<string, unknown>): Promise<T>;
|
|
13
|
+
/**
|
|
14
|
+
* Make a POST request with retry logic
|
|
15
|
+
*/
|
|
16
|
+
post<T>(endpoint: string, data?: Record<string, unknown>): Promise<T>;
|
|
17
|
+
/**
|
|
18
|
+
* Make a DELETE request with retry logic
|
|
19
|
+
*/
|
|
20
|
+
delete<T>(endpoint: string): Promise<T>;
|
|
21
|
+
/**
|
|
22
|
+
* Make a request with exponential backoff retry logic
|
|
23
|
+
*/
|
|
24
|
+
private requestWithRetry;
|
|
25
|
+
/**
|
|
26
|
+
* Sleep for specified milliseconds
|
|
27
|
+
*/
|
|
28
|
+
private sleep;
|
|
29
|
+
/**
|
|
30
|
+
* Validate API key by making a test request
|
|
31
|
+
*/
|
|
32
|
+
validateApiKey(): Promise<boolean>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Try to fetch a task by ID from all known API endpoints.
|
|
36
|
+
* Tries endpoints in priority order until one succeeds.
|
|
37
|
+
* Useful when the task type is unknown.
|
|
38
|
+
*/
|
|
39
|
+
export declare function fetchTaskByIdFromKnownEndpoints(client: MeshyClient, taskId: string): Promise<{
|
|
40
|
+
task: GetTaskResponse;
|
|
41
|
+
endpoint: string;
|
|
42
|
+
} | null>;
|
|
43
|
+
/**
|
|
44
|
+
* Fetch a task, trying the given endpoint first, then falling back to auto-inference.
|
|
45
|
+
* Returns the task data and the resolved endpoint.
|
|
46
|
+
*/
|
|
47
|
+
export declare function getTaskWithAutoInference(client: MeshyClient, taskId: string, preferredEndpoint: string): Promise<{
|
|
48
|
+
task: GetTaskResponse;
|
|
49
|
+
endpoint: string;
|
|
50
|
+
}>;
|
|
51
|
+
/**
|
|
52
|
+
* Create and validate Meshy client
|
|
53
|
+
*/
|
|
54
|
+
export declare function createMeshyClient(): Promise<MeshyClient>;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meshy API client with authentication and error handling
|
|
3
|
+
*/
|
|
4
|
+
import axios from "axios";
|
|
5
|
+
import { API_BASE_URL, API_TIMEOUT, RETRY_DELAYS, MAX_RETRIES } from "../constants.js";
|
|
6
|
+
import { isRetryableError, isRateLimitError } from "./error-handler.js";
|
|
7
|
+
export class MeshyClient {
|
|
8
|
+
client;
|
|
9
|
+
apiKey;
|
|
10
|
+
constructor(apiKey) {
|
|
11
|
+
this.apiKey = apiKey;
|
|
12
|
+
this.client = axios.create({
|
|
13
|
+
baseURL: API_BASE_URL,
|
|
14
|
+
timeout: API_TIMEOUT,
|
|
15
|
+
headers: {
|
|
16
|
+
"Content-Type": "application/json",
|
|
17
|
+
"Accept": "application/json"
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
// Add request interceptor to inject auth token
|
|
21
|
+
this.client.interceptors.request.use((config) => {
|
|
22
|
+
config.headers.Authorization = `Bearer ${this.apiKey}`;
|
|
23
|
+
return config;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Make a GET request with retry logic
|
|
28
|
+
*/
|
|
29
|
+
async get(endpoint, params) {
|
|
30
|
+
return this.requestWithRetry({
|
|
31
|
+
method: "GET",
|
|
32
|
+
url: endpoint,
|
|
33
|
+
params
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Make a POST request with retry logic
|
|
38
|
+
*/
|
|
39
|
+
async post(endpoint, data) {
|
|
40
|
+
return this.requestWithRetry({
|
|
41
|
+
method: "POST",
|
|
42
|
+
url: endpoint,
|
|
43
|
+
data
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Make a DELETE request with retry logic
|
|
48
|
+
*/
|
|
49
|
+
async delete(endpoint) {
|
|
50
|
+
return this.requestWithRetry({
|
|
51
|
+
method: "DELETE",
|
|
52
|
+
url: endpoint
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Make a request with exponential backoff retry logic
|
|
57
|
+
*/
|
|
58
|
+
async requestWithRetry(config, retryCount = 0) {
|
|
59
|
+
try {
|
|
60
|
+
const response = await this.client.request(config);
|
|
61
|
+
return response.data;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
// Check if we should retry
|
|
65
|
+
const shouldRetry = retryCount < MAX_RETRIES &&
|
|
66
|
+
(isRetryableError(error) || isRateLimitError(error));
|
|
67
|
+
if (shouldRetry) {
|
|
68
|
+
const delay = RETRY_DELAYS[retryCount] || RETRY_DELAYS[RETRY_DELAYS.length - 1];
|
|
69
|
+
// Log retry attempt to stderr (not stdout for stdio transport)
|
|
70
|
+
console.error(`Request failed, retrying in ${delay}ms (attempt ${retryCount + 1}/${MAX_RETRIES})...`);
|
|
71
|
+
await this.sleep(delay);
|
|
72
|
+
return this.requestWithRetry(config, retryCount + 1);
|
|
73
|
+
}
|
|
74
|
+
// No more retries, throw the error
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Sleep for specified milliseconds
|
|
80
|
+
*/
|
|
81
|
+
sleep(ms) {
|
|
82
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Validate API key by making a test request
|
|
86
|
+
*/
|
|
87
|
+
async validateApiKey() {
|
|
88
|
+
try {
|
|
89
|
+
// Make a simple request to check if API key is valid
|
|
90
|
+
// Use the text-to-3d endpoint which returns a list of tasks
|
|
91
|
+
await this.get("/openapi/v2/text-to-3d");
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Try to fetch a task by ID from all known API endpoints.
|
|
101
|
+
* Tries endpoints in priority order until one succeeds.
|
|
102
|
+
* Useful when the task type is unknown.
|
|
103
|
+
*/
|
|
104
|
+
export async function fetchTaskByIdFromKnownEndpoints(client, taskId) {
|
|
105
|
+
const endpoints = [
|
|
106
|
+
"/openapi/v2/text-to-3d",
|
|
107
|
+
"/openapi/v1/image-to-3d",
|
|
108
|
+
"/openapi/v1/multi-image-to-3d",
|
|
109
|
+
"/openapi/v1/remesh",
|
|
110
|
+
"/openapi/v1/retexture",
|
|
111
|
+
"/openapi/v1/rigging",
|
|
112
|
+
"/openapi/v1/animations",
|
|
113
|
+
"/openapi/v1/text-to-image",
|
|
114
|
+
"/openapi/v1/image-to-image",
|
|
115
|
+
"/openapi/v1/print/multi-color"
|
|
116
|
+
];
|
|
117
|
+
for (const endpoint of endpoints) {
|
|
118
|
+
try {
|
|
119
|
+
const task = await client.get(`${endpoint}/${taskId}`);
|
|
120
|
+
if (task && task.id) {
|
|
121
|
+
return { task, endpoint };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Not found on this endpoint, try next
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Fetch a task, trying the given endpoint first, then falling back to auto-inference.
|
|
133
|
+
* Returns the task data and the resolved endpoint.
|
|
134
|
+
*/
|
|
135
|
+
export async function getTaskWithAutoInference(client, taskId, preferredEndpoint) {
|
|
136
|
+
// Try preferred endpoint first
|
|
137
|
+
try {
|
|
138
|
+
const task = await client.get(`${preferredEndpoint}/${taskId}`);
|
|
139
|
+
if (task && task.id) {
|
|
140
|
+
return { task, endpoint: preferredEndpoint };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Fall through to auto-inference
|
|
145
|
+
}
|
|
146
|
+
// Auto-infer from all endpoints
|
|
147
|
+
const result = await fetchTaskByIdFromKnownEndpoints(client, taskId);
|
|
148
|
+
if (result) {
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
throw new Error(`Task ${taskId} not found on any endpoint. Verify the task_id is correct.`);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Create and validate Meshy client
|
|
155
|
+
*/
|
|
156
|
+
export async function createMeshyClient() {
|
|
157
|
+
const apiKey = process.env.MESHY_API_KEY;
|
|
158
|
+
if (!apiKey) {
|
|
159
|
+
throw new Error("MESHY_API_KEY environment variable is required. " +
|
|
160
|
+
"Get your API key from https://www.meshy.ai/settings/api");
|
|
161
|
+
}
|
|
162
|
+
const client = new MeshyClient(apiKey);
|
|
163
|
+
// Validate API key on startup
|
|
164
|
+
console.error("Validating Meshy API key...");
|
|
165
|
+
const isValid = await client.validateApiKey();
|
|
166
|
+
if (!isValid) {
|
|
167
|
+
throw new Error("Invalid MESHY_API_KEY. Please check your API key is correct. " +
|
|
168
|
+
"Get your API key from https://www.meshy.ai/settings/api");
|
|
169
|
+
}
|
|
170
|
+
console.error("✓ Meshy API key validated successfully");
|
|
171
|
+
return client;
|
|
172
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
export interface TaskRecord {
|
|
11
|
+
task_id: string;
|
|
12
|
+
task_type: string;
|
|
13
|
+
stage: string;
|
|
14
|
+
prompt?: string;
|
|
15
|
+
status: string;
|
|
16
|
+
files: string[];
|
|
17
|
+
created_at: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Infer a human-readable stage name from task type and API response type field.
|
|
21
|
+
*/
|
|
22
|
+
export declare function inferStage(taskType: string, apiType?: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Resolve (or create) the project directory for a task.
|
|
25
|
+
*
|
|
26
|
+
* For chained tasks (refine → preview, rig → source), pass parentTaskId
|
|
27
|
+
* to place the output in the same project folder as the parent.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveProjectDir(taskId: string, taskType: string, prompt?: string, parentTaskId?: string, createdAt?: string | number): string;
|
|
30
|
+
/**
|
|
31
|
+
* Generate the file path for a model download within a project directory.
|
|
32
|
+
* Returns: /path/to/project/stage.ext (e.g., preview.glb, refined.glb)
|
|
33
|
+
*/
|
|
34
|
+
export declare function getFilePath(projectDir: string, stage: string, format: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Generate texture file path within a project directory.
|
|
37
|
+
* Returns: /path/to/project/stage_texType.ext (e.g., refined_base_color.png)
|
|
38
|
+
*/
|
|
39
|
+
export declare function getTextureFilePath(projectDir: string, stage: string, textureType: string, url: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Record a completed task into the project's metadata.json.
|
|
42
|
+
*/
|
|
43
|
+
export declare function recordTask(projectDir: string, record: TaskRecord): void;
|
|
44
|
+
/**
|
|
45
|
+
* Download and save thumbnail to the project directory.
|
|
46
|
+
* Silently skips on failure.
|
|
47
|
+
*/
|
|
48
|
+
export declare function saveThumbnail(projectDir: string, thumbnailUrl: string): Promise<string | null>;
|
|
49
|
+
/**
|
|
50
|
+
* Get the output root path (for display purposes).
|
|
51
|
+
*/
|
|
52
|
+
export declare function getOutputRootPath(): string;
|