@wavespeed/mcp 1.0.6 → 1.0.8
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 +15 -3
- package/dist/lib/api.js +34 -14
- package/dist/lib/config.js +16 -8
- package/dist/lib/validate-inputs.js +62 -0
- package/dist/server.js +96 -14
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
|
|
10
10
|
# WaveSpeed MCP Server
|
|
11
11
|
|
|
12
|
+
<a href="https://cursor.com/install-mcp?name=wavespeed&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkB3YXZlc3BlZWQvbWNwIl0sImVudiI6eyJXQVZFU1BFRURfQVBJX0tFWSI6Indza18uLi4ifX0%3D">
|
|
13
|
+
<picture>
|
|
14
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/deeplink/mcp-install-light.svg">
|
|
15
|
+
<img src="https://cursor.com/deeplink/mcp-install-dark.svg" alt="Add wavespeed MCP server to Cursor" height="32"/>
|
|
16
|
+
</picture>
|
|
17
|
+
</a>
|
|
18
|
+
|
|
12
19
|
<!-- mcp-name: ai.wavespeed/mcp -->
|
|
13
20
|
|
|
14
21
|
An MCP server for the [WaveSpeed](https://wavespeed.ai) platform: run any model on the **live catalog** — image, video, audio, 3D — from Claude Code, Claude Desktop, Cursor, Cline, or any MCP client.
|
|
@@ -26,13 +33,13 @@ Built on the same design as the open-source [`@wavespeed/cli`](https://github.co
|
|
|
26
33
|
|---|---|
|
|
27
34
|
| `list_models` | Search the live catalog by text or modality type |
|
|
28
35
|
| `get_model_schema` | A model's real input schema (required, properties, defaults) |
|
|
29
|
-
| `run_model` | Submit + wait; `@path` inputs upload automatically; returns output URLs |
|
|
36
|
+
| `run_model` | Submit + wait; rejects input keys the model doesn't accept; `@path` inputs upload automatically; returns output URLs |
|
|
30
37
|
| `get_price` | Cost estimate with `unpriced_inputs` / `at_base_price` disclosure |
|
|
31
38
|
| `get_balance` | Account credit balance |
|
|
32
39
|
| `upload_file` | Local file → hosted URL (24h content-hash dedupe) |
|
|
33
|
-
| `get_prediction` |
|
|
40
|
+
| `get_prediction` | Status/outputs of any run by id; optional `wait_seconds` to wait for it |
|
|
34
41
|
|
|
35
|
-
|
|
42
|
+
Some generations take minutes to hours. `run_model` waits up to `wait_seconds` (default 50, max 1800) and then returns `done: false` with the prediction id — the task keeps running server-side, and `get_prediction` picks it up. While waiting, both tools send MCP progress notifications that carry the prediction id.
|
|
36
43
|
|
|
37
44
|
## Setup
|
|
38
45
|
|
|
@@ -76,3 +83,8 @@ npm run build # tsc → dist/
|
|
|
76
83
|
## License
|
|
77
84
|
|
|
78
85
|
[MIT](LICENSE)
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
**[WaveSpeed AI](https://wavespeed.ai/)** — AI image & video generation platform.
|
|
90
|
+
Try it in the browser: **[Image generator](https://wavespeed.ai/image-generator)** · **[Video generator](https://wavespeed.ai/video-generator)**
|
package/dist/lib/api.js
CHANGED
|
@@ -14,10 +14,11 @@ const { version: PKG_VERSION } = require('../../package.json');
|
|
|
14
14
|
// following the wavespeed-desktop convention. The WAVESPEED_CLIENT_NAME
|
|
15
15
|
// environment variable overrides the name so wrapper channels can brand
|
|
16
16
|
// themselves without code changes.
|
|
17
|
+
export const MCP_CLIENT_NAME = 'wavespeed-mcp';
|
|
17
18
|
export function clientAttributionHeaders() {
|
|
18
19
|
const platform = os.platform();
|
|
19
20
|
return {
|
|
20
|
-
'X-Client-Name': process.env.WAVESPEED_CLIENT_NAME ||
|
|
21
|
+
'X-Client-Name': process.env.WAVESPEED_CLIENT_NAME || MCP_CLIENT_NAME,
|
|
21
22
|
'X-Client-Version': PKG_VERSION,
|
|
22
23
|
'X-Client-OS': platform === 'win32' ? 'windows' : platform,
|
|
23
24
|
};
|
|
@@ -33,7 +34,7 @@ function readCache(baseUrl) {
|
|
|
33
34
|
return null;
|
|
34
35
|
if (Date.now() - parsed.fetched_at > CACHE_TTL_MS)
|
|
35
36
|
return null;
|
|
36
|
-
return parsed
|
|
37
|
+
return parsed;
|
|
37
38
|
}
|
|
38
39
|
catch {
|
|
39
40
|
return null;
|
|
@@ -96,15 +97,27 @@ async function apiPost(apiPath, body) {
|
|
|
96
97
|
throw new Error(json.message || `API returned code ${json.code}`);
|
|
97
98
|
return json.data;
|
|
98
99
|
}
|
|
100
|
+
// The server is a long-lived process, so keep the parsed catalog in memory
|
|
101
|
+
// instead of re-reading the ~2MB disk cache on every tool call. Same 1h TTL;
|
|
102
|
+
// the disk cache still lets a fresh process start without a network fetch.
|
|
103
|
+
let memCache = null;
|
|
99
104
|
export async function fetchModels(opts = {}) {
|
|
100
105
|
const baseUrl = getBaseUrl();
|
|
101
106
|
if (!opts.refresh) {
|
|
107
|
+
if (memCache &&
|
|
108
|
+
memCache.baseUrl === baseUrl &&
|
|
109
|
+
Date.now() - memCache.fetchedAt <= CACHE_TTL_MS) {
|
|
110
|
+
return memCache.models;
|
|
111
|
+
}
|
|
102
112
|
const cached = readCache(baseUrl);
|
|
103
|
-
if (cached)
|
|
104
|
-
|
|
113
|
+
if (cached) {
|
|
114
|
+
memCache = { baseUrl, fetchedAt: cached.fetched_at, models: cached.models };
|
|
115
|
+
return cached.models;
|
|
116
|
+
}
|
|
105
117
|
}
|
|
106
118
|
const models = await apiGet('/api/v3/models');
|
|
107
119
|
writeCache(baseUrl, models);
|
|
120
|
+
memCache = { baseUrl, fetchedAt: Date.now(), models };
|
|
108
121
|
return models;
|
|
109
122
|
}
|
|
110
123
|
export async function fetchBalance() {
|
|
@@ -125,21 +138,28 @@ export async function submitPrediction(model, input) {
|
|
|
125
138
|
export async function fetchPrediction(id) {
|
|
126
139
|
return apiGet(`/api/v3/predictions/${id}/result`);
|
|
127
140
|
}
|
|
128
|
-
|
|
141
|
+
export const TERMINAL_FAILURES = ['failed', 'cancelled', 'timeout', 'deleted'];
|
|
142
|
+
/**
|
|
143
|
+
* Poll a prediction until it reaches a terminal status (done=true, whether
|
|
144
|
+
* completed or failed — the caller decides what a failure means) or the wait
|
|
145
|
+
* budget runs out. Running out is NOT an error: generations can take hours,
|
|
146
|
+
* so the caller gets the still-running prediction back (done=false) and
|
|
147
|
+
* reports its id instead of losing it in a thrown message. `onTick` fires
|
|
148
|
+
* after every non-terminal poll.
|
|
149
|
+
*/
|
|
129
150
|
export async function waitForPrediction(id, opts = {}) {
|
|
130
151
|
const interval = opts.intervalMs ?? 1000;
|
|
131
|
-
const deadline = opts.timeoutMs ? Date.now() + opts.timeoutMs : undefined;
|
|
152
|
+
const deadline = opts.timeoutMs !== undefined ? Date.now() + opts.timeoutMs : undefined;
|
|
132
153
|
for (;;) {
|
|
133
154
|
const item = await fetchPrediction(id);
|
|
134
|
-
if (item.status === 'completed')
|
|
135
|
-
return item;
|
|
136
|
-
if (item.status === 'failed' || item.status === 'cancelled' || item.status === 'timeout') {
|
|
137
|
-
throw new Error(`Prediction ${item.status}${item.error ? `: ${item.error}` : ''} (task_id: ${id})`);
|
|
138
|
-
}
|
|
139
|
-
if (deadline && Date.now() > deadline) {
|
|
140
|
-
throw new Error(`Still ${item.status} after the wait limit (task_id: ${id}). ` +
|
|
141
|
-
`The task keeps running server-side — check it later with get_prediction.`);
|
|
155
|
+
if (item.status === 'completed' || TERMINAL_FAILURES.includes(item.status)) {
|
|
156
|
+
return { item, done: true };
|
|
142
157
|
}
|
|
158
|
+
await opts.onTick?.(item);
|
|
159
|
+
if (opts.signal?.aborted)
|
|
160
|
+
return { item, done: false };
|
|
161
|
+
if (deadline !== undefined && Date.now() + interval > deadline)
|
|
162
|
+
return { item, done: false };
|
|
143
163
|
await new Promise((r) => setTimeout(r, interval));
|
|
144
164
|
}
|
|
145
165
|
}
|
package/dist/lib/config.js
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
// API key + base URL resolution.
|
|
2
2
|
//
|
|
3
3
|
// Precedence: WAVESPEED_API_KEY / WAVESPEED_BASE_URL env vars, then the
|
|
4
|
-
// wavespeed CLI's stored config (
|
|
5
|
-
//
|
|
4
|
+
// wavespeed CLI's stored config (written by `wavespeed login`; location is
|
|
5
|
+
// per-platform, see cliConfigPath). Reading the CLI's store means one login
|
|
6
6
|
// covers both tools — the MCP server never implements its own auth flow.
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import os from 'node:os';
|
|
10
10
|
export const DEFAULT_BASE_URL = 'https://api.wavespeed.ai';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
// Mirrors conf's env-paths resolution (projectName "wavespeed", suffix
|
|
12
|
+
// "nodejs"), which is per-platform — a Linux-only path would miss every
|
|
13
|
+
// `wavespeed login` done on macOS or Windows.
|
|
14
|
+
export function cliConfigPath(platform = process.platform, env = process.env, home = os.homedir()) {
|
|
15
|
+
const name = 'wavespeed-nodejs';
|
|
16
|
+
if (platform === 'darwin') {
|
|
17
|
+
return path.join(home, 'Library', 'Preferences', name, 'config.json');
|
|
18
|
+
}
|
|
19
|
+
if (platform === 'win32') {
|
|
20
|
+
const appData = env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
21
|
+
return path.join(appData, name, 'Config', 'config.json');
|
|
22
|
+
}
|
|
23
|
+
const base = env.XDG_CONFIG_HOME || path.join(home, '.config');
|
|
24
|
+
return path.join(base, name, 'config.json');
|
|
17
25
|
}
|
|
18
26
|
function readCliConfig() {
|
|
19
27
|
try {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Pre-submit input validation against a model's published request schema.
|
|
2
|
+
//
|
|
3
|
+
// The API's entry whitelist silently DROPS any input key that is not in the
|
|
4
|
+
// model's schema — a typo like `size=2560*1440` against a model whose real
|
|
5
|
+
// knobs are `aspect_ratio`/`resolution` doesn't fail, it just quietly bills a
|
|
6
|
+
// generation with defaults. Catching that before submission is the only
|
|
7
|
+
// moment the mistake is still free.
|
|
8
|
+
//
|
|
9
|
+
// Validation fails OPEN: no schema (brand-new model, catalog lag, offline
|
|
10
|
+
// cache miss) means no check. It must never block a submission the API would
|
|
11
|
+
// have accepted.
|
|
12
|
+
export function requestSchemaProps(model) {
|
|
13
|
+
const props = model?.api_schema?.api_schemas?.[0]?.request_schema?.properties;
|
|
14
|
+
if (!props)
|
|
15
|
+
return null;
|
|
16
|
+
const keys = Object.keys(props);
|
|
17
|
+
return keys.length > 0 ? keys : null;
|
|
18
|
+
}
|
|
19
|
+
// Small edit distance for typo hints — inputs are short snake_case tokens, so
|
|
20
|
+
// a full Levenshtein over ≤40 keys is nothing.
|
|
21
|
+
function editDistance(a, b) {
|
|
22
|
+
const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
23
|
+
for (let j = 0; j <= b.length; j++)
|
|
24
|
+
dp[0][j] = j;
|
|
25
|
+
for (let i = 1; i <= a.length; i++)
|
|
26
|
+
for (let j = 1; j <= b.length; j++)
|
|
27
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
28
|
+
return dp[a.length][b.length];
|
|
29
|
+
}
|
|
30
|
+
function closest(key, known) {
|
|
31
|
+
let best;
|
|
32
|
+
let bestD = Infinity;
|
|
33
|
+
for (const k of known) {
|
|
34
|
+
const d = editDistance(key.toLowerCase(), k.toLowerCase());
|
|
35
|
+
if (d < bestD) {
|
|
36
|
+
bestD = d;
|
|
37
|
+
best = k;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// A hint is only a hint when it's plausibly the same word.
|
|
41
|
+
return bestD <= Math.max(2, Math.floor(key.length / 3)) ? best : undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Compare top-level input keys with the schema's properties.
|
|
45
|
+
* Returns null when everything checks out or no schema is available.
|
|
46
|
+
*/
|
|
47
|
+
export function findUnknownInputs(input, model) {
|
|
48
|
+
const known = requestSchemaProps(model);
|
|
49
|
+
if (!known)
|
|
50
|
+
return null;
|
|
51
|
+
const knownSet = new Set(known);
|
|
52
|
+
const unknown = Object.keys(input).filter((k) => !knownSet.has(k));
|
|
53
|
+
if (unknown.length === 0)
|
|
54
|
+
return null;
|
|
55
|
+
const suggestions = new Map();
|
|
56
|
+
for (const k of unknown) {
|
|
57
|
+
const hit = closest(k, known);
|
|
58
|
+
if (hit)
|
|
59
|
+
suggestions.set(k, hit);
|
|
60
|
+
}
|
|
61
|
+
return { unknown, known, suggestions };
|
|
62
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -15,11 +15,12 @@ import { createRequire } from 'node:module';
|
|
|
15
15
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
16
16
|
import { z } from 'zod';
|
|
17
17
|
import { Client } from 'wavespeed';
|
|
18
|
-
import { fetchModels, fetchBalance, fetchPricing,
|
|
18
|
+
import { fetchModels, fetchBalance, fetchPricing, submitPrediction, waitForPrediction, MCP_CLIENT_NAME, TERMINAL_FAILURES, } from './lib/api.js';
|
|
19
19
|
import { getApiKey, getBaseUrl } from './lib/config.js';
|
|
20
20
|
import { resolveLocalFiles } from './lib/local-files.js';
|
|
21
21
|
import { uploadWithCache } from './lib/upload-cache.js';
|
|
22
22
|
import { missingPriceVars, isFloorQuote } from './lib/pricing-vars.js';
|
|
23
|
+
import { findUnknownInputs } from './lib/validate-inputs.js';
|
|
23
24
|
const require = createRequire(import.meta.url);
|
|
24
25
|
const { version: PKG_VERSION } = require('../package.json');
|
|
25
26
|
const PRICE_DISCLAIMER = 'Estimate only, for reference — the amount actually charged for a run is authoritative.';
|
|
@@ -39,9 +40,63 @@ async function uploadFile(filePath) {
|
|
|
39
40
|
const apiKey = getApiKey();
|
|
40
41
|
if (!apiKey)
|
|
41
42
|
throw new Error('No API key configured (WAVESPEED_API_KEY or `wavespeed login`).');
|
|
42
|
-
const client = new Client(apiKey, { baseUrl: getBaseUrl() });
|
|
43
|
+
const client = new Client(apiKey, { baseUrl: getBaseUrl(), clientName: MCP_CLIENT_NAME });
|
|
43
44
|
return uploadWithCache(filePath, (p) => client.upload(p));
|
|
44
45
|
}
|
|
46
|
+
// Default wait for run_model. Many MCP clients abort a tool call after 60s,
|
|
47
|
+
// and an aborted call never delivers the prediction id — so return before
|
|
48
|
+
// that with the id in hand. Longer waits are opt-in via wait_seconds.
|
|
49
|
+
const DEFAULT_WAIT_SECONDS = 50;
|
|
50
|
+
const POLL_INTERVAL_MS = 2000;
|
|
51
|
+
const STILL_RUNNING_HINT = 'Still running on the server — generations can take minutes to hours. ' +
|
|
52
|
+
'Call get_prediction with this id (optionally with wait_seconds) to check again.';
|
|
53
|
+
// Emits notifications/progress while polling when the client asked for it.
|
|
54
|
+
// Each one carries the prediction id, and clients that reset their timeout
|
|
55
|
+
// on progress keep a long wait alive.
|
|
56
|
+
function progressTicker(extra, id, startedAt) {
|
|
57
|
+
const token = extra._meta?.progressToken;
|
|
58
|
+
if (token === undefined)
|
|
59
|
+
return undefined;
|
|
60
|
+
let n = 0;
|
|
61
|
+
return async (item) => {
|
|
62
|
+
const secs = Math.floor((Date.now() - startedAt) / 1000);
|
|
63
|
+
await extra
|
|
64
|
+
.sendNotification({
|
|
65
|
+
method: 'notifications/progress',
|
|
66
|
+
params: { progressToken: token, progress: ++n, message: `prediction ${id}: ${item.status} (${secs}s)` },
|
|
67
|
+
})
|
|
68
|
+
.catch(() => {
|
|
69
|
+
/* progress is advisory */
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Reject inputs the model's schema does not declare. The API silently drops
|
|
74
|
+
// unknown keys and still bills the run, so this is the last free moment to
|
|
75
|
+
// catch an invented parameter. Fails open when no schema is available, and
|
|
76
|
+
// re-fetches the catalog once before rejecting so a stale cache can't block
|
|
77
|
+
// a valid input.
|
|
78
|
+
async function assertKnownInputs(model, input) {
|
|
79
|
+
let report;
|
|
80
|
+
try {
|
|
81
|
+
report = findUnknownInputs(input, (await fetchModels()).find((m) => m.model_id === model));
|
|
82
|
+
if (report) {
|
|
83
|
+
const fresh = await fetchModels({ refresh: true });
|
|
84
|
+
report = findUnknownInputs(input, fresh.find((m) => m.model_id === model));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return; // catalog unreachable — validation is best-effort
|
|
89
|
+
}
|
|
90
|
+
if (!report)
|
|
91
|
+
return;
|
|
92
|
+
const hints = report.unknown.map((k) => {
|
|
93
|
+
const s = report.suggestions.get(k);
|
|
94
|
+
return s ? `${k} (did you mean ${s}?)` : k;
|
|
95
|
+
});
|
|
96
|
+
throw new Error(`Model ${model} does not accept: ${hints.join(', ')}. The API silently drops unknown ` +
|
|
97
|
+
`inputs and still bills the run, so nothing was submitted. Accepted inputs: ` +
|
|
98
|
+
`${report.known.join(', ')}. See get_model_schema.`);
|
|
99
|
+
}
|
|
45
100
|
function ok(payload) {
|
|
46
101
|
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
47
102
|
}
|
|
@@ -53,7 +108,9 @@ export function createServer() {
|
|
|
53
108
|
'inputs, run_model to execute. Reference local files as "@./path"',
|
|
54
109
|
'values inside input — they upload automatically; bare paths are',
|
|
55
110
|
'passed through untouched and will fail model validation.',
|
|
56
|
-
'Use get_price before expensive runs. Never invent model IDs.',
|
|
111
|
+
'Use get_price before expensive runs. Never invent model IDs or input keys.',
|
|
112
|
+
'Long generations (video, some audio/3D) can run for hours: when',
|
|
113
|
+
'run_model returns done=false, keep the id and poll get_prediction.',
|
|
57
114
|
].join(' '),
|
|
58
115
|
});
|
|
59
116
|
server.tool('list_models', 'Search the live WaveSpeed model catalog (image, video, audio, 3D). Returns model IDs usable with run_model. Do not invent model IDs — always pick one returned by this tool.', {
|
|
@@ -97,7 +154,7 @@ export function createServer() {
|
|
|
97
154
|
property_order: schema?.['x-order-properties'] ?? [],
|
|
98
155
|
});
|
|
99
156
|
});
|
|
100
|
-
server.tool('run_model', 'Run any WaveSpeed model. input keys come from get_model_schema. Local files: pass "@./path" string values — they are uploaded and replaced with hosted URLs (bare paths are NOT uploaded). Returns output URLs.
|
|
157
|
+
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" string values — they are uploaded and replaced with hosted URLs (bare paths are NOT uploaded). Returns output URLs when done=true. Some generations take minutes to hours: if the wait ends first, the result has done=false and the prediction id — the task keeps running, so check it with get_prediction.', {
|
|
101
158
|
model: z.string().describe('Model ID from list_models'),
|
|
102
159
|
input: z
|
|
103
160
|
.record(z.unknown())
|
|
@@ -107,28 +164,36 @@ export function createServer() {
|
|
|
107
164
|
.int()
|
|
108
165
|
.min(0)
|
|
109
166
|
.max(1800)
|
|
110
|
-
.default(
|
|
111
|
-
.describe(
|
|
112
|
-
}, { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ model, input, wait_seconds }) => {
|
|
167
|
+
.default(DEFAULT_WAIT_SECONDS)
|
|
168
|
+
.describe(`Max seconds to wait before returning the still-running prediction id (default ${DEFAULT_WAIT_SECONDS}); 0 = submit only`),
|
|
169
|
+
}, { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ model, input, wait_seconds }, extra) => {
|
|
113
170
|
const resolved = await resolveLocalFiles(input, {
|
|
114
171
|
upload: async (p) => (await uploadFile(p)).url,
|
|
115
172
|
});
|
|
173
|
+
await assertKnownInputs(model, resolved.input);
|
|
116
174
|
const started = Date.now();
|
|
117
175
|
const submitted = await submitPrediction(model, resolved.input);
|
|
118
176
|
if (wait_seconds === 0) {
|
|
119
|
-
return ok({ id: submitted.id, status: submitted.status, model });
|
|
177
|
+
return ok({ id: submitted.id, status: submitted.status, model, done: false, next: STILL_RUNNING_HINT });
|
|
120
178
|
}
|
|
121
|
-
const done = await waitForPrediction(submitted.id, {
|
|
122
|
-
intervalMs:
|
|
179
|
+
const { item, done } = await waitForPrediction(submitted.id, {
|
|
180
|
+
intervalMs: POLL_INTERVAL_MS,
|
|
123
181
|
timeoutMs: wait_seconds * 1000,
|
|
182
|
+
onTick: progressTicker(extra, submitted.id, started),
|
|
183
|
+
signal: extra.signal,
|
|
124
184
|
});
|
|
185
|
+
if (TERMINAL_FAILURES.includes(item.status)) {
|
|
186
|
+
throw new Error(`Prediction ${item.status}${item.error ? `: ${item.error}` : ''} (task_id: ${submitted.id})`);
|
|
187
|
+
}
|
|
125
188
|
return ok({
|
|
126
189
|
id: submitted.id,
|
|
127
190
|
model,
|
|
128
|
-
status:
|
|
129
|
-
|
|
191
|
+
status: item.status,
|
|
192
|
+
done,
|
|
193
|
+
outputs: item.outputs ?? [],
|
|
130
194
|
elapsed_ms: Date.now() - started,
|
|
131
195
|
uploaded_files: resolved.uploaded,
|
|
196
|
+
...(done ? {} : { next: STILL_RUNNING_HINT }),
|
|
132
197
|
});
|
|
133
198
|
});
|
|
134
199
|
server.tool('get_price', 'Estimate the cost of a run before executing it (no charge). Provide the same input you would pass to run_model — pricing often depends on inputs like duration or resolution.', {
|
|
@@ -172,15 +237,32 @@ export function createServer() {
|
|
|
172
237
|
server.tool('upload_file', 'Upload a local file to WaveSpeed and get its hosted URL (identical bytes reuse the same upload for 24h). Usually unnecessary — run_model handles "@./path" inputs itself.', { path: z.string().describe('Local file path') }, { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ path: filePath }) => {
|
|
173
238
|
return ok(await uploadFile(filePath));
|
|
174
239
|
});
|
|
175
|
-
server.tool('get_prediction', 'Fetch the status and outputs of a past or in-flight prediction by id — use to
|
|
176
|
-
|
|
240
|
+
server.tool('get_prediction', 'Fetch the status and outputs of a past or in-flight prediction by id — use to pick up a run that returned done=false. Pass wait_seconds to keep waiting for it to finish.', {
|
|
241
|
+
id: z.string().describe('Prediction id returned by run_model'),
|
|
242
|
+
wait_seconds: z
|
|
243
|
+
.number()
|
|
244
|
+
.int()
|
|
245
|
+
.min(0)
|
|
246
|
+
.max(1800)
|
|
247
|
+
.default(0)
|
|
248
|
+
.describe('Max seconds to wait for a terminal status; 0 = return the current status now'),
|
|
249
|
+
}, { readOnlyHint: true, openWorldHint: true }, async ({ id, wait_seconds }, extra) => {
|
|
250
|
+
const started = Date.now();
|
|
251
|
+
const { item, done } = await waitForPrediction(id, {
|
|
252
|
+
intervalMs: POLL_INTERVAL_MS,
|
|
253
|
+
timeoutMs: wait_seconds * 1000,
|
|
254
|
+
onTick: wait_seconds > 0 ? progressTicker(extra, id, started) : undefined,
|
|
255
|
+
signal: extra.signal,
|
|
256
|
+
});
|
|
177
257
|
return ok({
|
|
178
258
|
id: item.id,
|
|
179
259
|
model: item.model,
|
|
180
260
|
status: item.status,
|
|
261
|
+
done,
|
|
181
262
|
outputs: item.outputs ?? [],
|
|
182
263
|
error: item.error,
|
|
183
264
|
created_at: item.created_at,
|
|
265
|
+
...(done ? {} : { next: STILL_RUNNING_HINT }),
|
|
184
266
|
});
|
|
185
267
|
});
|
|
186
268
|
return server;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wavespeed/mcp",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "WaveSpeed MCP server
|
|
3
|
+
"version": "1.0.8",
|
|
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": {
|
|
7
7
|
"wavespeed-mcp": "dist/index.js"
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
],
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
42
|
-
"wavespeed": "^0.2
|
|
42
|
+
"wavespeed": "^0.3.2",
|
|
43
43
|
"zod": "^3.24.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|