@warlok-net/mcp-server 0.1.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/README.md +40 -0
- package/dist/chunk-HTA6LITF.mjs +678 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +719 -0
- package/dist/cli.mjs +32 -0
- package/dist/index.d.mts +18 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +706 -0
- package/dist/index.mjs +8 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# @warlok-net/mcp-server
|
|
2
|
+
|
|
3
|
+
MCP server for [Warlok](https://warlok.net) — gives agents the ability to summon
|
|
4
|
+
game-ready, auto-rigged 3D assets from images or text prompts.
|
|
5
|
+
|
|
6
|
+
## Setup
|
|
7
|
+
|
|
8
|
+
Create an API key at [warlok.net/settings/api-keys](https://warlok.net/settings/api-keys), then:
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"mcpServers": {
|
|
13
|
+
"warlok": {
|
|
14
|
+
"command": "npx",
|
|
15
|
+
"args": ["-y", "@warlok-net/mcp-server"],
|
|
16
|
+
"env": {
|
|
17
|
+
"WARLOK_API_KEY": "<your_key>"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Tools
|
|
25
|
+
|
|
26
|
+
| Tool | Purpose |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| `generate_3d_model` | Summon a 3D asset from a prompt or image URL (1 credit = $1) |
|
|
29
|
+
| `get_model_status` | Poll stage/progress (~8 min end-to-end; poll every 10–15 s) |
|
|
30
|
+
| `list_models` | List the account's generations |
|
|
31
|
+
| `get_model_download_url` | Signed GLB/preview URLs for a completed asset |
|
|
32
|
+
| `delete_model` | Remove a generation |
|
|
33
|
+
| `capture_ui_screenshot` | Optional; requires a local Playwright install |
|
|
34
|
+
|
|
35
|
+
## Alpha limits
|
|
36
|
+
|
|
37
|
+
Prepaid credits ($1/summon), 5 summons per rolling day, failed summons auto-refunded.
|
|
38
|
+
`402` → buy credits at [warlok.net/settings](https://warlok.net/settings); `429` → daily cap reached.
|
|
39
|
+
|
|
40
|
+
Docs: [warlok.net/docs/mcp](https://warlok.net/docs/mcp) · License: MIT
|
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import {
|
|
5
|
+
CallToolRequestSchema,
|
|
6
|
+
ListToolsRequestSchema,
|
|
7
|
+
ErrorCode,
|
|
8
|
+
McpError
|
|
9
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
import {
|
|
11
|
+
WarlokClient
|
|
12
|
+
} from "@warlok-net/sdk";
|
|
13
|
+
|
|
14
|
+
// src/screenshot.ts
|
|
15
|
+
import fs from "fs/promises";
|
|
16
|
+
import path from "path";
|
|
17
|
+
async function loadChromium() {
|
|
18
|
+
try {
|
|
19
|
+
const playwright = await import("playwright");
|
|
20
|
+
return playwright.chromium;
|
|
21
|
+
} catch {
|
|
22
|
+
throw new Error(
|
|
23
|
+
"capture_ui_screenshot requires Playwright. Install it with: npm i playwright && npx playwright install chromium"
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
var DEFAULT_VIEWPORT_WIDTH = 1440;
|
|
28
|
+
var DEFAULT_VIEWPORT_HEIGHT = 900;
|
|
29
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
30
|
+
var DEFAULT_WAIT_MS = 250;
|
|
31
|
+
var DEFAULT_FORMAT = "png";
|
|
32
|
+
async function captureUiScreenshot(input) {
|
|
33
|
+
const startedAt = Date.now();
|
|
34
|
+
const url = parseHttpUrl(input.url);
|
|
35
|
+
const width = clampInteger(input.viewportWidth, 320, 7680, DEFAULT_VIEWPORT_WIDTH);
|
|
36
|
+
const height = clampInteger(
|
|
37
|
+
input.viewportHeight,
|
|
38
|
+
240,
|
|
39
|
+
4320,
|
|
40
|
+
DEFAULT_VIEWPORT_HEIGHT
|
|
41
|
+
);
|
|
42
|
+
const timeoutMs = clampInteger(input.timeoutMs, 5e3, 12e4, DEFAULT_TIMEOUT_MS);
|
|
43
|
+
const waitForMs = clampInteger(input.waitForMs, 0, 3e4, DEFAULT_WAIT_MS);
|
|
44
|
+
const fullPage = input.fullPage ?? true;
|
|
45
|
+
const colorScheme = input.colorScheme ?? "dark";
|
|
46
|
+
const format = input.format ?? inferFormatFromOutputPath(input.outputPath);
|
|
47
|
+
const outputPath = resolveOutputPath(url, input.outputPath, format);
|
|
48
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
49
|
+
const chromium = await loadChromium();
|
|
50
|
+
const browser = await chromium.launch({ headless: true });
|
|
51
|
+
try {
|
|
52
|
+
const context = await browser.newContext({
|
|
53
|
+
viewport: { width, height },
|
|
54
|
+
colorScheme,
|
|
55
|
+
deviceScaleFactor: 1
|
|
56
|
+
});
|
|
57
|
+
const page = await context.newPage();
|
|
58
|
+
page.setDefaultTimeout(timeoutMs);
|
|
59
|
+
await page.goto(url.toString(), { waitUntil: "networkidle", timeout: timeoutMs });
|
|
60
|
+
if (input.waitForSelector) {
|
|
61
|
+
await page.waitForSelector(input.waitForSelector, {
|
|
62
|
+
state: "visible",
|
|
63
|
+
timeout: timeoutMs
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (waitForMs > 0) {
|
|
67
|
+
await page.waitForTimeout(waitForMs);
|
|
68
|
+
}
|
|
69
|
+
if (input.selector) {
|
|
70
|
+
const locator = page.locator(input.selector).first();
|
|
71
|
+
await locator.waitFor({ state: "visible", timeout: timeoutMs });
|
|
72
|
+
await locator.screenshot({
|
|
73
|
+
path: outputPath,
|
|
74
|
+
type: format
|
|
75
|
+
});
|
|
76
|
+
} else {
|
|
77
|
+
await page.screenshot({
|
|
78
|
+
path: outputPath,
|
|
79
|
+
fullPage,
|
|
80
|
+
type: format
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
await context.close();
|
|
84
|
+
} finally {
|
|
85
|
+
await browser.close();
|
|
86
|
+
}
|
|
87
|
+
const stat = await fs.stat(outputPath);
|
|
88
|
+
return {
|
|
89
|
+
outputPath,
|
|
90
|
+
bytes: stat.size,
|
|
91
|
+
width,
|
|
92
|
+
height,
|
|
93
|
+
url: url.toString(),
|
|
94
|
+
selector: input.selector ?? null,
|
|
95
|
+
fullPage,
|
|
96
|
+
colorScheme,
|
|
97
|
+
format,
|
|
98
|
+
durationMs: Date.now() - startedAt
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function parseHttpUrl(value) {
|
|
102
|
+
let parsed;
|
|
103
|
+
try {
|
|
104
|
+
parsed = new URL(value);
|
|
105
|
+
} catch {
|
|
106
|
+
throw new Error("Invalid URL. Expected a valid absolute URL.");
|
|
107
|
+
}
|
|
108
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
109
|
+
throw new Error("Invalid URL protocol. Only http/https URLs are supported.");
|
|
110
|
+
}
|
|
111
|
+
return parsed;
|
|
112
|
+
}
|
|
113
|
+
function resolveOutputPath(url, requestedPath, format) {
|
|
114
|
+
if (requestedPath) {
|
|
115
|
+
const absolute = path.isAbsolute(requestedPath) ? requestedPath : path.resolve(outputRoot(), requestedPath);
|
|
116
|
+
return ensureExt(absolute, format);
|
|
117
|
+
}
|
|
118
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
119
|
+
const host = sanitizeSegment(url.host);
|
|
120
|
+
const pathname = sanitizeSegment(url.pathname.replace(/\//g, "_") || "root");
|
|
121
|
+
const filename = `${now}-${host}-${pathname}.${format}`;
|
|
122
|
+
return path.join(outputRoot(), filename);
|
|
123
|
+
}
|
|
124
|
+
function outputRoot() {
|
|
125
|
+
const configured = process.env.WARLOK_SCREENSHOT_DIR;
|
|
126
|
+
if (configured && configured.trim()) {
|
|
127
|
+
return path.resolve(configured.trim());
|
|
128
|
+
}
|
|
129
|
+
return path.resolve(process.cwd(), "tmp", "mcp-screenshots");
|
|
130
|
+
}
|
|
131
|
+
function ensureExt(value, format) {
|
|
132
|
+
const ext = path.extname(value).toLowerCase();
|
|
133
|
+
if (ext === ".png" || ext === ".jpg" || ext === ".jpeg" || ext === ".webp") {
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
return `${value}.${format}`;
|
|
137
|
+
}
|
|
138
|
+
function sanitizeSegment(value) {
|
|
139
|
+
return value.toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 120) || "capture";
|
|
140
|
+
}
|
|
141
|
+
function inferFormatFromOutputPath(pathname) {
|
|
142
|
+
if (!pathname) return DEFAULT_FORMAT;
|
|
143
|
+
const ext = path.extname(pathname).toLowerCase();
|
|
144
|
+
if (ext === ".jpg" || ext === ".jpeg") return "jpeg";
|
|
145
|
+
return "png";
|
|
146
|
+
}
|
|
147
|
+
function clampInteger(value, min, max, fallback) {
|
|
148
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
149
|
+
const rounded = Math.round(value);
|
|
150
|
+
if (rounded < min) return min;
|
|
151
|
+
if (rounded > max) return max;
|
|
152
|
+
return rounded;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/index.ts
|
|
156
|
+
var TOOLS = [
|
|
157
|
+
{
|
|
158
|
+
name: "generate_3d_model",
|
|
159
|
+
description: "Generate a 3D model from text description or image. Returns an asset_id to track status.",
|
|
160
|
+
inputSchema: {
|
|
161
|
+
type: "object",
|
|
162
|
+
properties: {
|
|
163
|
+
name: {
|
|
164
|
+
type: "string",
|
|
165
|
+
description: "Name for the generation (max 100 characters)"
|
|
166
|
+
},
|
|
167
|
+
type: {
|
|
168
|
+
type: "string",
|
|
169
|
+
enum: [
|
|
170
|
+
"character",
|
|
171
|
+
"creature",
|
|
172
|
+
"environment",
|
|
173
|
+
"prop",
|
|
174
|
+
"weapon",
|
|
175
|
+
"vehicle",
|
|
176
|
+
"material",
|
|
177
|
+
"noun",
|
|
178
|
+
"person",
|
|
179
|
+
"place",
|
|
180
|
+
"thing"
|
|
181
|
+
],
|
|
182
|
+
description: "Entity or legacy asset type"
|
|
183
|
+
},
|
|
184
|
+
prompt: {
|
|
185
|
+
type: "string",
|
|
186
|
+
description: "Text prompt describing what to generate"
|
|
187
|
+
},
|
|
188
|
+
image_url: {
|
|
189
|
+
type: "string",
|
|
190
|
+
description: "Optional URL to a reference image"
|
|
191
|
+
},
|
|
192
|
+
auto_rig: {
|
|
193
|
+
type: "boolean",
|
|
194
|
+
description: "Whether to rig for Mixamo when generating a person",
|
|
195
|
+
default: true
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
required: ["type"]
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: "get_model_status",
|
|
203
|
+
description: "Check generation status by asset_id.",
|
|
204
|
+
inputSchema: {
|
|
205
|
+
type: "object",
|
|
206
|
+
properties: {
|
|
207
|
+
asset_id: {
|
|
208
|
+
type: "string",
|
|
209
|
+
description: "The generation ID"
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
required: ["asset_id"]
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: "list_models",
|
|
217
|
+
description: "List generations for the current account.",
|
|
218
|
+
inputSchema: {
|
|
219
|
+
type: "object",
|
|
220
|
+
properties: {
|
|
221
|
+
limit: {
|
|
222
|
+
type: "number",
|
|
223
|
+
description: "Maximum rows to return (default: 20)",
|
|
224
|
+
default: 20
|
|
225
|
+
},
|
|
226
|
+
type: {
|
|
227
|
+
type: "string",
|
|
228
|
+
enum: [
|
|
229
|
+
"character",
|
|
230
|
+
"creature",
|
|
231
|
+
"environment",
|
|
232
|
+
"prop",
|
|
233
|
+
"weapon",
|
|
234
|
+
"vehicle",
|
|
235
|
+
"material",
|
|
236
|
+
"noun",
|
|
237
|
+
"person",
|
|
238
|
+
"place",
|
|
239
|
+
"thing"
|
|
240
|
+
],
|
|
241
|
+
description: "Filter by entity/legacy type"
|
|
242
|
+
},
|
|
243
|
+
status: {
|
|
244
|
+
type: "string",
|
|
245
|
+
enum: [
|
|
246
|
+
"pending",
|
|
247
|
+
"preprocessing",
|
|
248
|
+
"generating",
|
|
249
|
+
"rigging",
|
|
250
|
+
"complete",
|
|
251
|
+
"failed"
|
|
252
|
+
],
|
|
253
|
+
description: "Filter by generation status"
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: "get_model_download_url",
|
|
260
|
+
description: "Get the best available download URL for a completed generation.",
|
|
261
|
+
inputSchema: {
|
|
262
|
+
type: "object",
|
|
263
|
+
properties: {
|
|
264
|
+
asset_id: {
|
|
265
|
+
type: "string",
|
|
266
|
+
description: "The generation ID"
|
|
267
|
+
},
|
|
268
|
+
format: {
|
|
269
|
+
type: "string",
|
|
270
|
+
enum: ["model", "rigged"],
|
|
271
|
+
description: "Preferred format (default: best available)"
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
required: ["asset_id"]
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: "delete_model",
|
|
279
|
+
description: "Delete a generation.",
|
|
280
|
+
inputSchema: {
|
|
281
|
+
type: "object",
|
|
282
|
+
properties: {
|
|
283
|
+
asset_id: {
|
|
284
|
+
type: "string",
|
|
285
|
+
description: "The generation ID to delete"
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
required: ["asset_id"]
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
name: "capture_ui_screenshot",
|
|
293
|
+
description: "Capture a deterministic Playwright screenshot for UI/design iteration.",
|
|
294
|
+
inputSchema: {
|
|
295
|
+
type: "object",
|
|
296
|
+
properties: {
|
|
297
|
+
url: {
|
|
298
|
+
type: "string",
|
|
299
|
+
description: "HTTP/HTTPS URL to capture"
|
|
300
|
+
},
|
|
301
|
+
output_path: {
|
|
302
|
+
type: "string",
|
|
303
|
+
description: "Optional output file path. Relative paths are resolved in tmp/mcp-screenshots."
|
|
304
|
+
},
|
|
305
|
+
selector: {
|
|
306
|
+
type: "string",
|
|
307
|
+
description: "Optional CSS selector to capture only a specific element."
|
|
308
|
+
},
|
|
309
|
+
wait_for_selector: {
|
|
310
|
+
type: "string",
|
|
311
|
+
description: "Optional selector to wait for before capture (useful for async UI)."
|
|
312
|
+
},
|
|
313
|
+
wait_for_ms: {
|
|
314
|
+
type: "number",
|
|
315
|
+
description: "Extra delay before capture in milliseconds (default: 250).",
|
|
316
|
+
default: 250
|
|
317
|
+
},
|
|
318
|
+
timeout_ms: {
|
|
319
|
+
type: "number",
|
|
320
|
+
description: "Navigation and selector timeout in milliseconds (default: 30000).",
|
|
321
|
+
default: 3e4
|
|
322
|
+
},
|
|
323
|
+
full_page: {
|
|
324
|
+
type: "boolean",
|
|
325
|
+
description: "Capture full page when selector is not set (default: true).",
|
|
326
|
+
default: true
|
|
327
|
+
},
|
|
328
|
+
viewport_width: {
|
|
329
|
+
type: "number",
|
|
330
|
+
description: "Viewport width in px (default: 1440).",
|
|
331
|
+
default: 1440
|
|
332
|
+
},
|
|
333
|
+
viewport_height: {
|
|
334
|
+
type: "number",
|
|
335
|
+
description: "Viewport height in px (default: 900).",
|
|
336
|
+
default: 900
|
|
337
|
+
},
|
|
338
|
+
color_scheme: {
|
|
339
|
+
type: "string",
|
|
340
|
+
enum: ["light", "dark", "no-preference"],
|
|
341
|
+
description: "Preferred color scheme (default: dark).",
|
|
342
|
+
default: "dark"
|
|
343
|
+
},
|
|
344
|
+
format: {
|
|
345
|
+
type: "string",
|
|
346
|
+
enum: ["png", "jpeg"],
|
|
347
|
+
description: "Screenshot format (default inferred from output_path, else png)."
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
required: ["url"]
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
];
|
|
354
|
+
function createServer(apiKey, baseUrl) {
|
|
355
|
+
const client = new WarlokClient({ apiKey, baseUrl });
|
|
356
|
+
const server = new Server(
|
|
357
|
+
{
|
|
358
|
+
name: "warlok-mcp",
|
|
359
|
+
version: "0.1.0"
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
capabilities: {
|
|
363
|
+
tools: {}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
);
|
|
367
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
368
|
+
tools: TOOLS
|
|
369
|
+
}));
|
|
370
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
371
|
+
const { name, arguments: args } = request.params;
|
|
372
|
+
try {
|
|
373
|
+
switch (name) {
|
|
374
|
+
case "generate_3d_model": {
|
|
375
|
+
const generation = await client.generate({
|
|
376
|
+
name: stringOrUndefined(args, "name"),
|
|
377
|
+
type: requiredType(args),
|
|
378
|
+
prompt: stringOrUndefined(args, "prompt"),
|
|
379
|
+
imageUrl: stringOrUndefined(args, "image_url"),
|
|
380
|
+
options: {
|
|
381
|
+
autoRig: booleanOrUndefined(args, "auto_rig")
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
return {
|
|
385
|
+
content: [
|
|
386
|
+
{
|
|
387
|
+
type: "text",
|
|
388
|
+
text: JSON.stringify(
|
|
389
|
+
{
|
|
390
|
+
success: true,
|
|
391
|
+
asset_id: generation.id,
|
|
392
|
+
name: generation.name,
|
|
393
|
+
entity_type: generation.entityType,
|
|
394
|
+
status: generation.status,
|
|
395
|
+
message: `Generation started. Use get_model_status with asset_id "${generation.id}".`
|
|
396
|
+
},
|
|
397
|
+
null,
|
|
398
|
+
2
|
|
399
|
+
)
|
|
400
|
+
}
|
|
401
|
+
]
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
case "get_model_status": {
|
|
405
|
+
const generation = await client.getGeneration(requiredAssetId(args));
|
|
406
|
+
return {
|
|
407
|
+
content: [
|
|
408
|
+
{
|
|
409
|
+
type: "text",
|
|
410
|
+
text: JSON.stringify(formatGenerationStatus(generation), null, 2)
|
|
411
|
+
}
|
|
412
|
+
]
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
case "list_models": {
|
|
416
|
+
const generations = await client.listGenerations({
|
|
417
|
+
limit: numberOrUndefined(args, "limit") || 20,
|
|
418
|
+
type: typeOrUndefined(args),
|
|
419
|
+
status: statusOrUndefined(args)
|
|
420
|
+
});
|
|
421
|
+
return {
|
|
422
|
+
content: [
|
|
423
|
+
{
|
|
424
|
+
type: "text",
|
|
425
|
+
text: JSON.stringify(
|
|
426
|
+
{
|
|
427
|
+
total: generations.length,
|
|
428
|
+
models: generations.map((generation) => ({
|
|
429
|
+
id: generation.id,
|
|
430
|
+
name: generation.name,
|
|
431
|
+
entity_type: generation.entityType,
|
|
432
|
+
status: generation.status,
|
|
433
|
+
created_at: generation.createdAt
|
|
434
|
+
}))
|
|
435
|
+
},
|
|
436
|
+
null,
|
|
437
|
+
2
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
]
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
case "get_model_download_url": {
|
|
444
|
+
const generation = await client.getGeneration(requiredAssetId(args));
|
|
445
|
+
if (generation.status !== "complete") {
|
|
446
|
+
return {
|
|
447
|
+
content: [
|
|
448
|
+
{
|
|
449
|
+
type: "text",
|
|
450
|
+
text: JSON.stringify({
|
|
451
|
+
success: false,
|
|
452
|
+
error: `Generation is not complete. Current status: ${generation.status}`
|
|
453
|
+
})
|
|
454
|
+
}
|
|
455
|
+
]
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
const format = downloadFormatOrUndefined(args);
|
|
459
|
+
const url = format === "model" ? generation.outputs.modelUrl : format === "rigged" ? generation.outputs.riggedModelUrl : generation.outputs.riggedModelUrl || generation.outputs.modelUrl;
|
|
460
|
+
if (!url) {
|
|
461
|
+
return {
|
|
462
|
+
content: [
|
|
463
|
+
{
|
|
464
|
+
type: "text",
|
|
465
|
+
text: JSON.stringify({
|
|
466
|
+
success: false,
|
|
467
|
+
error: "No download URL available for this generation"
|
|
468
|
+
})
|
|
469
|
+
}
|
|
470
|
+
]
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
content: [
|
|
475
|
+
{
|
|
476
|
+
type: "text",
|
|
477
|
+
text: JSON.stringify(
|
|
478
|
+
{
|
|
479
|
+
success: true,
|
|
480
|
+
asset_id: generation.id,
|
|
481
|
+
name: generation.name,
|
|
482
|
+
download_url: url,
|
|
483
|
+
format: format || "best_available",
|
|
484
|
+
prompt: generation.input.prompt || null,
|
|
485
|
+
preview_png_url: generation.outputs.previewPngUrl || null,
|
|
486
|
+
nanobanana_png_url: generation.outputs.promptImageUrl || generation.provenance?.preprocessPromptImageUrl || null
|
|
487
|
+
},
|
|
488
|
+
null,
|
|
489
|
+
2
|
|
490
|
+
)
|
|
491
|
+
}
|
|
492
|
+
]
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
case "delete_model": {
|
|
496
|
+
const assetId = requiredAssetId(args);
|
|
497
|
+
await client.deleteGeneration(assetId);
|
|
498
|
+
return {
|
|
499
|
+
content: [
|
|
500
|
+
{
|
|
501
|
+
type: "text",
|
|
502
|
+
text: JSON.stringify({
|
|
503
|
+
success: true,
|
|
504
|
+
message: `Generation ${assetId} deleted successfully`
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
]
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
case "capture_ui_screenshot": {
|
|
511
|
+
const screenshot = await captureUiScreenshot({
|
|
512
|
+
url: requiredUrl(args),
|
|
513
|
+
outputPath: stringOrUndefined(args, "output_path"),
|
|
514
|
+
selector: stringOrUndefined(args, "selector"),
|
|
515
|
+
waitForSelector: stringOrUndefined(args, "wait_for_selector"),
|
|
516
|
+
waitForMs: numberOrUndefined(args, "wait_for_ms"),
|
|
517
|
+
timeoutMs: numberOrUndefined(args, "timeout_ms"),
|
|
518
|
+
fullPage: booleanOrUndefined(args, "full_page"),
|
|
519
|
+
viewportWidth: numberOrUndefined(args, "viewport_width"),
|
|
520
|
+
viewportHeight: numberOrUndefined(args, "viewport_height"),
|
|
521
|
+
colorScheme: colorSchemeOrUndefined(args),
|
|
522
|
+
format: screenshotFormatOrUndefined(args)
|
|
523
|
+
});
|
|
524
|
+
return {
|
|
525
|
+
content: [
|
|
526
|
+
{
|
|
527
|
+
type: "text",
|
|
528
|
+
text: JSON.stringify(
|
|
529
|
+
{
|
|
530
|
+
success: true,
|
|
531
|
+
...screenshot
|
|
532
|
+
},
|
|
533
|
+
null,
|
|
534
|
+
2
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
]
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
default:
|
|
541
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
542
|
+
}
|
|
543
|
+
} catch (error) {
|
|
544
|
+
if (error instanceof McpError) {
|
|
545
|
+
throw error;
|
|
546
|
+
}
|
|
547
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
548
|
+
return {
|
|
549
|
+
content: [
|
|
550
|
+
{
|
|
551
|
+
type: "text",
|
|
552
|
+
text: JSON.stringify({ success: false, error: message })
|
|
553
|
+
}
|
|
554
|
+
],
|
|
555
|
+
isError: true
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
return server;
|
|
560
|
+
}
|
|
561
|
+
function requiredAssetId(args) {
|
|
562
|
+
const value = stringOrUndefined(args, "asset_id");
|
|
563
|
+
if (!value) {
|
|
564
|
+
throw new McpError(ErrorCode.InvalidParams, "asset_id is required");
|
|
565
|
+
}
|
|
566
|
+
return value;
|
|
567
|
+
}
|
|
568
|
+
function requiredType(args) {
|
|
569
|
+
const value = stringOrUndefined(args, "type");
|
|
570
|
+
if (!value) {
|
|
571
|
+
throw new McpError(ErrorCode.InvalidParams, "type is required");
|
|
572
|
+
}
|
|
573
|
+
return value;
|
|
574
|
+
}
|
|
575
|
+
function requiredUrl(args) {
|
|
576
|
+
const value = stringOrUndefined(args, "url");
|
|
577
|
+
if (!value) {
|
|
578
|
+
throw new McpError(ErrorCode.InvalidParams, "url is required");
|
|
579
|
+
}
|
|
580
|
+
return value;
|
|
581
|
+
}
|
|
582
|
+
function typeOrUndefined(args) {
|
|
583
|
+
const value = stringOrUndefined(args, "type");
|
|
584
|
+
return value;
|
|
585
|
+
}
|
|
586
|
+
function statusOrUndefined(args) {
|
|
587
|
+
const value = stringOrUndefined(args, "status");
|
|
588
|
+
return value;
|
|
589
|
+
}
|
|
590
|
+
function downloadFormatOrUndefined(args) {
|
|
591
|
+
const value = stringOrUndefined(args, "format");
|
|
592
|
+
if (value === "model" || value === "rigged") {
|
|
593
|
+
return value;
|
|
594
|
+
}
|
|
595
|
+
return void 0;
|
|
596
|
+
}
|
|
597
|
+
function stringOrUndefined(args, key) {
|
|
598
|
+
const value = args?.[key];
|
|
599
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
600
|
+
}
|
|
601
|
+
function booleanOrUndefined(args, key) {
|
|
602
|
+
const value = args?.[key];
|
|
603
|
+
return typeof value === "boolean" ? value : void 0;
|
|
604
|
+
}
|
|
605
|
+
function numberOrUndefined(args, key) {
|
|
606
|
+
const value = args?.[key];
|
|
607
|
+
return typeof value === "number" ? value : void 0;
|
|
608
|
+
}
|
|
609
|
+
function colorSchemeOrUndefined(args) {
|
|
610
|
+
const value = stringOrUndefined(args, "color_scheme");
|
|
611
|
+
if (value === "light" || value === "dark" || value === "no-preference") {
|
|
612
|
+
return value;
|
|
613
|
+
}
|
|
614
|
+
return void 0;
|
|
615
|
+
}
|
|
616
|
+
function screenshotFormatOrUndefined(args) {
|
|
617
|
+
const value = stringOrUndefined(args, "format");
|
|
618
|
+
if (value === "png" || value === "jpeg") {
|
|
619
|
+
return value;
|
|
620
|
+
}
|
|
621
|
+
return void 0;
|
|
622
|
+
}
|
|
623
|
+
function formatGenerationStatus(generation) {
|
|
624
|
+
const base = {
|
|
625
|
+
success: true,
|
|
626
|
+
asset_id: generation.id,
|
|
627
|
+
name: generation.name,
|
|
628
|
+
entity_type: generation.entityType,
|
|
629
|
+
status: generation.status,
|
|
630
|
+
progress: generation.progress,
|
|
631
|
+
prompt: generation.input.prompt || null,
|
|
632
|
+
preview_png_url: generation.outputs.previewPngUrl || null,
|
|
633
|
+
nanobanana_png_url: generation.outputs.promptImageUrl || generation.provenance?.preprocessPromptImageUrl || null,
|
|
634
|
+
created_at: generation.createdAt,
|
|
635
|
+
updated_at: generation.updatedAt
|
|
636
|
+
};
|
|
637
|
+
if (generation.status === "complete") {
|
|
638
|
+
return {
|
|
639
|
+
...base,
|
|
640
|
+
model_url: generation.outputs.modelUrl,
|
|
641
|
+
rigged_model_url: generation.outputs.riggedModelUrl,
|
|
642
|
+
mixamo_ready: generation.outputs.mixamoReady
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
if (generation.status === "failed") {
|
|
646
|
+
return {
|
|
647
|
+
...base,
|
|
648
|
+
error: generation.error
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
return {
|
|
652
|
+
...base,
|
|
653
|
+
message: getStatusMessage(generation.status, generation.progress)
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
function getStatusMessage(status, progress) {
|
|
657
|
+
const messages = {
|
|
658
|
+
pending: "Queued for processing",
|
|
659
|
+
preprocessing: "Preparing specialized PNG",
|
|
660
|
+
generating: "Generating 3D mesh",
|
|
661
|
+
rigging: "Rigging for Mixamo"
|
|
662
|
+
};
|
|
663
|
+
return `${messages[status] || "Processing"} (${progress}% complete)`;
|
|
664
|
+
}
|
|
665
|
+
async function runServer(apiKey, baseUrl) {
|
|
666
|
+
const server = createServer(apiKey, baseUrl);
|
|
667
|
+
const transport = new StdioServerTransport();
|
|
668
|
+
await server.connect(transport);
|
|
669
|
+
process.on("SIGINT", async () => {
|
|
670
|
+
await server.close();
|
|
671
|
+
process.exit(0);
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
export {
|
|
676
|
+
createServer,
|
|
677
|
+
runServer
|
|
678
|
+
};
|