hydrilla-mcp 2.0.0 → 2.0.2
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/README.md +6 -3
- package/dist/api.js +20 -9
- package/dist/bridge.js +3 -3
- package/dist/config.js +1 -1
- package/dist/index.js +54 -21
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -91,7 +91,7 @@ The path is relative to the folder that contains `.mcp.json`. The server reads `
|
|
|
91
91
|
| `hydrilla_me` | Plan and credit balance. Free. |
|
|
92
92
|
| `hydrilla_text_to_3d` | Generate a GLB from a prompt. `send_to` places it in an open app. |
|
|
93
93
|
| `hydrilla_image_to_3d` | Generate a GLB from an image URL. `send_to` places it in an open app. |
|
|
94
|
-
| `hydrilla_get_task` |
|
|
94
|
+
| `hydrilla_get_task` | Waits up to ~45 s on one task, then returns status, progress and mesh URL. `send_to` places it in an app once ready. |
|
|
95
95
|
| `hydrilla_list_tasks` | Recent models on the account. Free. |
|
|
96
96
|
| `hydrilla_cancel_task` | Stop a running task; unused credits are refunded. |
|
|
97
97
|
| `hydrilla_list_apps` | Which apps have their Bridge running right now. |
|
|
@@ -105,5 +105,8 @@ Bridge running, `send_to: "blender"` on a generate tool, or `hydrilla_send_to_ap
|
|
|
105
105
|
the plugin download the GLB and import it into the open scene. If the Bridge is off, the
|
|
106
106
|
tool says which button to click and the model can be sent again without regenerating.
|
|
107
107
|
|
|
108
|
-
A
|
|
109
|
-
|
|
108
|
+
A model takes 10 to 20 minutes to generate. No tool call stays open longer than about a
|
|
109
|
+
minute: a generate call returns the task id with the progress so far, and the agent calls
|
|
110
|
+
`hydrilla_get_task` (which waits up to ~45 seconds per call) until the model is ready, telling
|
|
111
|
+
the user the progress in between. Calls also send progress notifications to clients that ask
|
|
112
|
+
for them. Hydrilla fails and refunds a task that runs longer than 20 minutes.
|
package/dist/api.js
CHANGED
|
@@ -2,7 +2,9 @@ import { API_KEYS_URL, SERVER_VERSION, resolveApiBase, resolveApiKey } from "./c
|
|
|
2
2
|
const REQUEST_TIMEOUT_MS = 60_000;
|
|
3
3
|
/** Text-to-3D creates its concept image before the request returns. */
|
|
4
4
|
export const CREATE_TIMEOUT_MS = 240_000;
|
|
5
|
-
const POLL_INTERVAL_MS =
|
|
5
|
+
const POLL_INTERVAL_MS = 3_000;
|
|
6
|
+
/** Shortest timeout given to one status request, so a nearly spent wait still gets an answer. */
|
|
7
|
+
const MIN_POLL_TIMEOUT_MS = 10_000;
|
|
6
8
|
const MAX_POLL_FAILURES = 5;
|
|
7
9
|
const NO_KEY_MESSAGE = `No Hydrilla API key found. Ask the user to add their key in the Hydrilla plugin (for example the Hydrilla tab in Blender), ` +
|
|
8
10
|
`or to set HYDRILLA_API_KEY in this MCP server's config. Keys are created at ${API_KEYS_URL}. ` +
|
|
@@ -85,10 +87,10 @@ export async function api(path, init = {}, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
|
85
87
|
}
|
|
86
88
|
return data;
|
|
87
89
|
}
|
|
88
|
-
export const getTask = (taskId) => api(`/v1/3d/tasks/${encodeURIComponent(taskId)}
|
|
89
|
-
const pollOnce = async (taskId) => {
|
|
90
|
+
export const getTask = (taskId, timeoutMs = REQUEST_TIMEOUT_MS) => api(`/v1/3d/tasks/${encodeURIComponent(taskId)}`, {}, timeoutMs);
|
|
91
|
+
const pollOnce = async (taskId, timeoutMs) => {
|
|
90
92
|
try {
|
|
91
|
-
return await getTask(taskId);
|
|
93
|
+
return await getTask(taskId, timeoutMs);
|
|
92
94
|
}
|
|
93
95
|
catch (error) {
|
|
94
96
|
// A brief network or server hiccup should not lose a paid task.
|
|
@@ -100,17 +102,22 @@ const pollOnce = async (taskId) => {
|
|
|
100
102
|
}
|
|
101
103
|
};
|
|
102
104
|
/**
|
|
103
|
-
* Poll until the task finishes or the wait budget runs out.
|
|
104
|
-
*
|
|
105
|
+
* Poll until the task finishes or the wait budget runs out. Always checks at
|
|
106
|
+
* least once, and no request outlives the deadline by more than
|
|
107
|
+
* MIN_POLL_TIMEOUT_MS. On timeout the task keeps running on Hydrilla and can
|
|
108
|
+
* be checked again with its id.
|
|
105
109
|
*/
|
|
106
110
|
export async function waitForTask(taskId, maxWaitMs, onProgress) {
|
|
107
111
|
const deadline = Date.now() + maxWaitMs;
|
|
108
112
|
const state = {
|
|
113
|
+
polls: 0,
|
|
109
114
|
failures: 0,
|
|
110
115
|
last: { id: taskId, status: "PENDING" },
|
|
111
116
|
};
|
|
112
|
-
while (Date.now() < deadline) {
|
|
113
|
-
|
|
117
|
+
while (state.polls === 0 || Date.now() < deadline) {
|
|
118
|
+
state.polls += 1;
|
|
119
|
+
const timeoutMs = Math.max(MIN_POLL_TIMEOUT_MS, Math.min(REQUEST_TIMEOUT_MS, deadline - Date.now()));
|
|
120
|
+
const task = await pollOnce(taskId, timeoutMs);
|
|
114
121
|
if (task) {
|
|
115
122
|
state.failures = 0;
|
|
116
123
|
state.last = task;
|
|
@@ -125,7 +132,11 @@ export async function waitForTask(taskId, maxWaitMs, onProgress) {
|
|
|
125
132
|
throw new HydrillaError("Lost contact with Hydrilla while waiting. Check the task again with hydrilla_get_task.");
|
|
126
133
|
}
|
|
127
134
|
}
|
|
128
|
-
|
|
135
|
+
const remainingMs = deadline - Date.now();
|
|
136
|
+
if (remainingMs <= 0) {
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
await sleep(Math.min(POLL_INTERVAL_MS, remainingMs));
|
|
129
140
|
}
|
|
130
141
|
return { task: state.last, timedOut: true };
|
|
131
142
|
}
|
package/dist/bridge.js
CHANGED
|
@@ -6,8 +6,8 @@ export const APPS = ["blender", "maya", "3dsmax", "unreal", "unity", "godot"];
|
|
|
6
6
|
const BRIDGE_HOST = "127.0.0.1";
|
|
7
7
|
const BRIDGE_SERVICE = "hydrilla-bridge";
|
|
8
8
|
const HEALTH_TIMEOUT_MS = 1_500;
|
|
9
|
-
/** The plugin downloads the mesh before it replies. */
|
|
10
|
-
const IMPORT_TIMEOUT_MS =
|
|
9
|
+
/** The plugin downloads the mesh before it replies; kept short so the tool call still answers. */
|
|
10
|
+
const IMPORT_TIMEOUT_MS = 2 * 60_000;
|
|
11
11
|
const BRIDGE_PORTS = {
|
|
12
12
|
blender: 7345,
|
|
13
13
|
maya: 7346,
|
|
@@ -73,7 +73,7 @@ const postImport = async (app, body) => {
|
|
|
73
73
|
}
|
|
74
74
|
catch (error) {
|
|
75
75
|
const reason = error instanceof Error ? error.message : String(error);
|
|
76
|
-
throw new BridgeError(`${APP_NAMES[app]}
|
|
76
|
+
throw new BridgeError(`${APP_NAMES[app]} did not confirm the import (${reason}). It may still be importing; ask the user to check the app before sending the model again.`);
|
|
77
77
|
}
|
|
78
78
|
};
|
|
79
79
|
const readBridgeError = async (response) => {
|
package/dist/config.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
export const SERVER_VERSION = "2.0.
|
|
5
|
+
export const SERVER_VERSION = "2.0.2";
|
|
6
6
|
export const DEFAULT_API_BASE = "https://hydrilla-ai-g1bm.vercel.app";
|
|
7
7
|
export const API_KEYS_URL = "https://hydrilla.ai/app/api-keys";
|
|
8
8
|
/** Hosts that do not serve the developer API (old backend, GPU gateway). */
|
package/dist/index.js
CHANGED
|
@@ -7,8 +7,14 @@ import { CREATE_TIMEOUT_MS, api, getTask, waitForTask } from "./api.js";
|
|
|
7
7
|
import { APPS, appName, listListeningApps, sendMeshToApp } from "./bridge.js";
|
|
8
8
|
import { SERVER_VERSION, getPostHogHost, getPostHogProjectToken, loadLocalEnv } from "./config.js";
|
|
9
9
|
loadLocalEnv();
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Longest a tool call waits on a running task before it answers. A generation
|
|
12
|
+
* takes 10 to 20 minutes, far longer than many clients let one tool call run
|
|
13
|
+
* (Codex stops at 60 seconds), and the user sees nothing while a call is open.
|
|
14
|
+
* So each call answers with the current progress, and the agent calls
|
|
15
|
+
* hydrilla_get_task again to keep waiting.
|
|
16
|
+
*/
|
|
17
|
+
const CALL_BUDGET_MS = 45_000;
|
|
12
18
|
const jsonResult = (value) => ({
|
|
13
19
|
content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
|
|
14
20
|
});
|
|
@@ -60,21 +66,22 @@ const deliver = async (task, app, name) => {
|
|
|
60
66
|
return { send_error: error instanceof Error ? error.message : String(error) };
|
|
61
67
|
}
|
|
62
68
|
};
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const { task, timedOut } = await waitForTask(taskId,
|
|
69
|
+
const STILL_GENERATING_NOTE = "Still generating; a model usually takes 10 to 20 minutes in total. Tell the user the progress so far, " +
|
|
70
|
+
"then call hydrilla_get_task with this task_id (and the same send_to and name, if any) to keep waiting. " +
|
|
71
|
+
"Do not start the generation again.";
|
|
72
|
+
const elapsedSecondsOf = (task) => typeof task.created_at === "number" ? Math.max(0, Math.round(Date.now() / 1000 - task.created_at)) : undefined;
|
|
73
|
+
/**
|
|
74
|
+
* Wait on a task for at most waitMs, then report where it stands. Once it has
|
|
75
|
+
* succeeded, place the model in the user's open app if asked.
|
|
76
|
+
*/
|
|
77
|
+
const awaitTask = async (taskId, options, extra) => {
|
|
78
|
+
const { task, timedOut } = await waitForTask(taskId, options.waitMs, progressReporter(extra));
|
|
73
79
|
if (timedOut) {
|
|
74
80
|
return jsonResult({
|
|
75
81
|
...task,
|
|
76
82
|
id: taskId,
|
|
77
|
-
|
|
83
|
+
elapsed_seconds: elapsedSecondsOf(task),
|
|
84
|
+
note: STILL_GENERATING_NOTE,
|
|
78
85
|
});
|
|
79
86
|
}
|
|
80
87
|
if (task.status !== "SUCCEEDED") {
|
|
@@ -83,6 +90,18 @@ const finishGeneration = async (taskId, options, extra) => {
|
|
|
83
90
|
const delivery = await deliver({ ...task, id: taskId }, options.sendTo, options.name);
|
|
84
91
|
return jsonResult({ ...task, id: taskId, ...delivery });
|
|
85
92
|
};
|
|
93
|
+
/** After the create request, wait only for what is left of this call's budget. */
|
|
94
|
+
const finishGeneration = async (taskId, startedAt, options, extra) => {
|
|
95
|
+
if (!options.wait) {
|
|
96
|
+
return jsonResult({
|
|
97
|
+
id: taskId,
|
|
98
|
+
status: "PENDING",
|
|
99
|
+
note: "Generation started; it usually takes 10 to 20 minutes. Check it with hydrilla_get_task (pass send_to there to place it in an app when it is done).",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const waitMs = Math.max(0, CALL_BUDGET_MS - (Date.now() - startedAt));
|
|
103
|
+
return awaitTask(taskId, { waitMs, sendTo: options.sendTo, name: options.name }, extra);
|
|
104
|
+
};
|
|
86
105
|
const server = new McpServer({
|
|
87
106
|
name: "hydrilla",
|
|
88
107
|
version: SERVER_VERSION,
|
|
@@ -135,27 +154,41 @@ const nameSchema = z
|
|
|
135
154
|
.optional()
|
|
136
155
|
.describe("Short name for the imported object, e.g. 'treasure-chest'");
|
|
137
156
|
server.tool("hydrilla_me", "Return the Hydrilla account plan and credit balance. Free; use it to check the connection.", {}, async () => guarded(async () => jsonResult(await api("/v1/user/me"))));
|
|
138
|
-
server.tool("hydrilla_text_to_3d", "Generate a textured 3D model (GLB) from a text prompt. Costs credits and takes a
|
|
157
|
+
server.tool("hydrilla_text_to_3d", "Generate a textured 3D model (GLB) from a text prompt. Costs credits and takes 10 to 20 minutes: the call returns a task_id within about a minute, then keep calling hydrilla_get_task with it until the model is ready. Set send_to to place it in the user's open Blender, Unity, Unreal, Godot, Maya or 3ds Max.", {
|
|
139
158
|
prompt: z.string().min(1).describe("What to generate"),
|
|
140
|
-
wait: z
|
|
159
|
+
wait: z
|
|
160
|
+
.boolean()
|
|
161
|
+
.optional()
|
|
162
|
+
.describe("Wait up to about 45 seconds for progress before answering (default true). The model is rarely ready that fast; keep checking with hydrilla_get_task."),
|
|
141
163
|
send_to: sendToSchema,
|
|
142
164
|
name: nameSchema,
|
|
143
165
|
}, async ({ prompt, wait = true, send_to, name }, extra) => guarded(async () => {
|
|
166
|
+
const startedAt = Date.now();
|
|
144
167
|
const created = await api("/v1/3d/text-to-3d", { method: "POST", body: JSON.stringify({ prompt }) }, CREATE_TIMEOUT_MS);
|
|
145
|
-
return finishGeneration(created.id, { wait, sendTo: send_to, name }, extra);
|
|
168
|
+
return finishGeneration(created.id, startedAt, { wait, sendTo: send_to, name }, extra);
|
|
146
169
|
}));
|
|
147
|
-
server.tool("hydrilla_image_to_3d", "Generate a textured 3D model (GLB) from a public image URL. Costs credits and takes a
|
|
170
|
+
server.tool("hydrilla_image_to_3d", "Generate a textured 3D model (GLB) from a public image URL. Costs credits and takes 10 to 20 minutes: the call returns a task_id within about a minute, then keep calling hydrilla_get_task with it until the model is ready. Set send_to to place it in the user's open app.", {
|
|
148
171
|
image_url: z.string().url().describe("Public image URL"),
|
|
149
|
-
wait: z
|
|
172
|
+
wait: z
|
|
173
|
+
.boolean()
|
|
174
|
+
.optional()
|
|
175
|
+
.describe("Wait up to about 45 seconds for progress before answering (default true). The model is rarely ready that fast; keep checking with hydrilla_get_task."),
|
|
150
176
|
send_to: sendToSchema,
|
|
151
177
|
name: nameSchema,
|
|
152
178
|
}, async ({ image_url, wait = true, send_to, name }, extra) => guarded(async () => {
|
|
179
|
+
const startedAt = Date.now();
|
|
153
180
|
const created = await api("/v1/3d/image-to-3d", { method: "POST", body: JSON.stringify({ image_url }) }, CREATE_TIMEOUT_MS);
|
|
154
|
-
return finishGeneration(created.id, { wait, sendTo: send_to, name }, extra);
|
|
181
|
+
return finishGeneration(created.id, startedAt, { wait, sendTo: send_to, name }, extra);
|
|
155
182
|
}));
|
|
156
|
-
server.tool("hydrilla_get_task", "Check a Hydrilla 3D task by id. Returns its status and, once it has succeeded, the mesh URL.", {
|
|
183
|
+
server.tool("hydrilla_get_task", "Check a Hydrilla 3D task by id, waiting up to about 45 seconds for it to finish. Returns its status and progress and, once it has succeeded, the mesh URL. While it is still generating, tell the user the progress and call this again with the same task_id. Pass send_to to place the model in an open app as soon as it is ready.", {
|
|
157
184
|
task_id: z.string().min(1),
|
|
158
|
-
|
|
185
|
+
wait: z
|
|
186
|
+
.boolean()
|
|
187
|
+
.optional()
|
|
188
|
+
.describe("Wait up to about 45 seconds for the task to finish (default true). Set false for an instant status check."),
|
|
189
|
+
send_to: sendToSchema,
|
|
190
|
+
name: nameSchema,
|
|
191
|
+
}, async ({ task_id, wait = true, send_to, name }, extra) => guarded(async () => awaitTask(task_id, { waitMs: wait ? CALL_BUDGET_MS : 0, sendTo: send_to, name }, extra)));
|
|
159
192
|
server.tool("hydrilla_list_tasks", "List the user's recent Hydrilla 3D models, newest first. Free. Use it to find a model that already exists instead of generating it again.", {
|
|
160
193
|
limit: z.number().int().min(1).max(100).optional().describe("How many to return (default 20)"),
|
|
161
194
|
}, async ({ limit = 20 }) => guarded(async () => jsonResult(await api(`/v1/3d/tasks?limit=${limit}`))));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hydrilla-mcp",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "MCP server for Hydrilla: generate 3D models and send them into Blender, Unity, Unreal, Godot, Maya or 3ds Max.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"license": "UNLICENSED",
|
|
19
19
|
"type": "module",
|
|
20
20
|
"bin": {
|
|
21
|
-
"hydrilla-mcp": "
|
|
21
|
+
"hydrilla-mcp": "dist/index.js"
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"dist",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"access": "public"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
|
-
"build": "tsc -p tsconfig.json",
|
|
31
|
+
"build": "tsc -p tsconfig.json && node -e \"require('fs').chmodSync('dist/index.js', 0o755)\"",
|
|
32
32
|
"start": "node dist/index.js",
|
|
33
33
|
"dev": "tsx src/index.ts",
|
|
34
34
|
"prepublishOnly": "npm run build"
|