@wavespeed/mcp 1.1.2 → 1.1.3
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/dist/lib/local-files.js +31 -7
- package/dist/server.js +4 -2
- package/package.json +1 -1
package/dist/lib/local-files.js
CHANGED
|
@@ -6,9 +6,30 @@
|
|
|
6
6
|
// asked to share based on a heuristic; a value that merely looks like a
|
|
7
7
|
// filename must never leave the machine. No `@`, no upload: bare paths pass
|
|
8
8
|
// through to the API untouched and fail model validation there if they were
|
|
9
|
-
// meant to be files.
|
|
9
|
+
// meant to be files. The `@` is only read in media fields (isMediaField):
|
|
10
|
+
// prompts legitimately start with "@Image 1".
|
|
10
11
|
import fs from 'node:fs';
|
|
11
12
|
import path from 'node:path';
|
|
13
|
+
// The same rule api-server (pkg/openapi IsMediaField) and the dashboard use to
|
|
14
|
+
// tell which inputs carry media, so a text field such as `prompt` that merely
|
|
15
|
+
// starts with "@" ("@Image 1 waves") is never read as a file path:
|
|
16
|
+
// - an enum is never media;
|
|
17
|
+
// - an explicit x-ui-component decides: uploader, uploaders and mask are
|
|
18
|
+
// media, any other component is not;
|
|
19
|
+
// - otherwise the name decides: image, *_image, *_image_url, video…, audio…
|
|
20
|
+
// (and their plurals), plus mask (api-server needs the uploader component
|
|
21
|
+
// for masks; the CLI also takes the bare name, which no text field uses).
|
|
22
|
+
const MEDIA_NAME = /(^|_)(image|image_url|video|video_url|audio|audio_url|images|image_urls|videos|video_urls|audios|audio_urls|mask)$/;
|
|
23
|
+
export function isMediaField(name, prop) {
|
|
24
|
+
const def = (prop && typeof prop === 'object' ? prop : {});
|
|
25
|
+
if (Array.isArray(def.enum) && def.enum.length > 0)
|
|
26
|
+
return false;
|
|
27
|
+
const component = def['x-ui-component'];
|
|
28
|
+
if (typeof component === 'string' && component !== '') {
|
|
29
|
+
return ['uploader', 'uploaders', 'mask'].includes(component.toLowerCase());
|
|
30
|
+
}
|
|
31
|
+
return MEDIA_NAME.test(name.toLowerCase());
|
|
32
|
+
}
|
|
12
33
|
function isRegularFile(p) {
|
|
13
34
|
try {
|
|
14
35
|
return fs.statSync(p).isFile();
|
|
@@ -21,24 +42,27 @@ function isRegularFile(p) {
|
|
|
21
42
|
* Walk the input and list every `@path` value. Pure over `exists`, so the
|
|
22
43
|
* rules can be tested without touching the disk.
|
|
23
44
|
*/
|
|
24
|
-
export function collectLocalFiles(input,
|
|
45
|
+
export function collectLocalFiles(input, opts = {}) {
|
|
25
46
|
const found = [];
|
|
26
|
-
|
|
47
|
+
// `media` is whether the nearest named field (an array's elements take the
|
|
48
|
+
// array's name) carries media.
|
|
49
|
+
const walk = (node, at, media, depth) => {
|
|
27
50
|
if (Array.isArray(node)) {
|
|
28
|
-
node.forEach((v, i) => walk(v, at ? `${at}.${i}` : String(i)));
|
|
51
|
+
node.forEach((v, i) => walk(v, at ? `${at}.${i}` : String(i), media, depth));
|
|
29
52
|
return;
|
|
30
53
|
}
|
|
31
54
|
if (node && typeof node === 'object') {
|
|
32
55
|
for (const [k, v] of Object.entries(node)) {
|
|
33
|
-
|
|
56
|
+
const prop = depth === 0 ? opts.props?.[k] : undefined;
|
|
57
|
+
walk(v, at ? `${at}.${k}` : k, opts.allFields || isMediaField(k, prop), depth + 1);
|
|
34
58
|
}
|
|
35
59
|
return;
|
|
36
60
|
}
|
|
37
|
-
if (typeof node !== 'string' || !node.startsWith('@'))
|
|
61
|
+
if (!media || typeof node !== 'string' || !node.startsWith('@'))
|
|
38
62
|
return;
|
|
39
63
|
found.push({ path: node.slice(1), at });
|
|
40
64
|
};
|
|
41
|
-
walk(input, '');
|
|
65
|
+
walk(input, '', !!opts.allFields, 0);
|
|
42
66
|
return found;
|
|
43
67
|
}
|
|
44
68
|
function setAt(root, dotted, value) {
|
package/dist/server.js
CHANGED
|
@@ -211,7 +211,7 @@ export function createServer() {
|
|
|
211
211
|
playground_url: modelPlaygroundUrl(meta.model_id),
|
|
212
212
|
});
|
|
213
213
|
});
|
|
214
|
-
server.tool('run_model', 'Run any WaveSpeed model. input keys come from get_model_schema; unknown keys are rejected before submission. Local files: pass "@./path"
|
|
214
|
+
server.tool('run_model', 'Run any WaveSpeed model. input keys come from get_model_schema; unknown keys are rejected before submission. Local files: pass "@./path" in a media field (image, images, video, audio, mask …) — it is uploaded and replaced with hosted URLs (bare paths are NOT uploaded). Returns the prediction id right away (done=false); call get_prediction with that id and wait_seconds until done=true to get the output URLs. The task keeps running on the server. Pass wait_seconds here to wait for a quick model in the same call.', {
|
|
215
215
|
model: z.string().describe('Model ID from list_models'),
|
|
216
216
|
input: z
|
|
217
217
|
.record(z.unknown())
|
|
@@ -225,6 +225,7 @@ export function createServer() {
|
|
|
225
225
|
.describe(`Max seconds to wait for the result in this call (default ${DEFAULT_WAIT_SECONDS}: return the prediction id right away)`),
|
|
226
226
|
}, { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ model, input, wait_seconds }, extra) => {
|
|
227
227
|
const resolved = await resolveLocalFiles(input, {
|
|
228
|
+
props: (await catalogProps(model).catch(() => null)) ?? undefined,
|
|
228
229
|
upload: async (p) => (await uploadFile(p)).url,
|
|
229
230
|
});
|
|
230
231
|
await assertKnownInputs(model, resolved.input);
|
|
@@ -289,7 +290,7 @@ export function createServer() {
|
|
|
289
290
|
for (const k of keys)
|
|
290
291
|
if (args[k] !== undefined)
|
|
291
292
|
media[k] = args[k];
|
|
292
|
-
const resolved = await resolveLocalFiles(media, { upload: async (p) => (await uploadFile(p)).url });
|
|
293
|
+
const resolved = await resolveLocalFiles(media, { allFields: true, upload: async (p) => (await uploadFile(p)).url });
|
|
293
294
|
return { args: { ...args, ...resolved.input }, uploaded: resolved.uploaded };
|
|
294
295
|
}
|
|
295
296
|
const imageShape = {
|
|
@@ -376,6 +377,7 @@ export function createServer() {
|
|
|
376
377
|
// would be wrong. The 24h content-hash cache means the later run reuses
|
|
377
378
|
// this upload.
|
|
378
379
|
const resolved = await resolveLocalFiles(input, {
|
|
380
|
+
props: (await catalogProps(model).catch(() => null)) ?? undefined,
|
|
379
381
|
upload: async (p) => (await uploadFile(p)).url,
|
|
380
382
|
});
|
|
381
383
|
const priceInput = resolved.input;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wavespeed/mcp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "WaveSpeed MCP server — run any model on the live WaveSpeed catalog (image, video, audio, 3D) with schema introspection, local-file upload, and price quotes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|