@wavespeed/mcp 1.0.13 → 1.0.15
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 +5 -4
- package/dist/lib/api.js +8 -0
- package/dist/lib/delete-predictions.js +34 -0
- package/dist/server.js +23 -32
- package/package.json +1 -1
- package/dist/lib/pricing-vars.js +0 -75
package/README.md
CHANGED
|
@@ -25,20 +25,21 @@ Built on the same design as the open-source [`@wavespeed/cli`](https://github.co
|
|
|
25
25
|
- **No hardcoded models.** The catalog comes from the live API (1h cache); new platform models work the day they ship.
|
|
26
26
|
- **One generation verb.** `run_model` executes anything; `get_model_schema` tells the agent what inputs a model actually accepts, so it reads before it writes.
|
|
27
27
|
- **Inputs are never mutated.** The one explicit transform is the `@path` marker — a `"@./photo.jpg"` value inside `input` uploads the file and substitutes its hosted URL. Bare paths are passed through untouched.
|
|
28
|
-
- **
|
|
28
|
+
- **Quote before you spend.** `get_price` takes the same input as `run_model` and returns an estimate; the amount actually charged for a run is authoritative.
|
|
29
29
|
|
|
30
30
|
## Tools
|
|
31
31
|
|
|
32
32
|
| Tool | What it does |
|
|
33
33
|
|---|---|
|
|
34
34
|
| `list_models` | Search the live catalog by text or modality type |
|
|
35
|
-
| `get_model_schema` | A model's real input schema (required, properties, defaults) |
|
|
35
|
+
| `get_model_schema` | A model's real input schema (required, properties, defaults), with a link to the model's `llms.txt` |
|
|
36
36
|
| `run_model` | Submit + wait; rejects input keys the model doesn't accept; `@path` inputs upload automatically; returns output URLs |
|
|
37
|
-
| `get_price` | Cost estimate with
|
|
38
|
-
| `get_balance` | Account credit balance |
|
|
37
|
+
| `get_price` | Cost estimate for the inputs you pass (no charge), with a link to the model's `llms.txt` |
|
|
38
|
+
| `get_balance` | Account credit balance, with the top-up link |
|
|
39
39
|
| `list_predictions` | Run history (default last 24h); filter by model, status, time range |
|
|
40
40
|
| `list_billings` | Itemized charges and refunds (needs a Billing-access key) |
|
|
41
41
|
| `get_usage` | Spend, request counts and per-model breakdown (default last 7 days; needs a Billing-access key) |
|
|
42
|
+
| `delete_predictions` | Delete finished runs from the history (up to 100; needs `confirm: true`, cannot be undone from here) |
|
|
42
43
|
| `upload_file` | Local file → hosted URL (24h content-hash dedupe) |
|
|
43
44
|
| `get_prediction` | Status/outputs of any run by id; optional `wait_seconds` to wait for it |
|
|
44
45
|
|
package/dist/lib/api.js
CHANGED
|
@@ -200,6 +200,10 @@ export async function fetchUsageStats(startTime, endTime, model) {
|
|
|
200
200
|
body.model_uuid = model;
|
|
201
201
|
return apiPost('/api/v3/user/usage_stats', body, { timeoutMs: GET_TIMEOUT_MS });
|
|
202
202
|
}
|
|
203
|
+
// The model page's markdown summary for agents (inputs, endpoint, examples).
|
|
204
|
+
export function modelLlmsTxtUrl(modelId) {
|
|
205
|
+
return `https://wavespeed.ai/models/${modelId}/llms.txt`;
|
|
206
|
+
}
|
|
203
207
|
export async function fetchPricing(modelId, inputs) {
|
|
204
208
|
// /model/price is the documented pricing endpoint.
|
|
205
209
|
return apiPost('/api/v3/model/price', { model_id: modelId, inputs });
|
|
@@ -256,3 +260,7 @@ export async function waitForPrediction(id, opts = {}) {
|
|
|
256
260
|
await new Promise((r) => setTimeout(r, interval));
|
|
257
261
|
}
|
|
258
262
|
}
|
|
263
|
+
// Deletes within the key's own organization; the response lists the ids actually deleted.
|
|
264
|
+
export async function deletePredictions(ids) {
|
|
265
|
+
return apiPost('/api/v3/predictions/delete', { ids }, { timeoutMs: GET_TIMEOUT_MS });
|
|
266
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// delete_predictions: the checks and the report around POST /api/v3/predictions/delete.
|
|
2
|
+
// The API only touches the key's own organization and returns the ids it deleted.
|
|
3
|
+
export const MAX_DELETE_IDS = 100;
|
|
4
|
+
const ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
5
|
+
/** The ids to send: trimmed, unique, valid, and only once the user agreed (confirm). */
|
|
6
|
+
export function prepareDelete(ids, confirm) {
|
|
7
|
+
const unique = [];
|
|
8
|
+
for (const raw of ids) {
|
|
9
|
+
const id = raw.trim();
|
|
10
|
+
if (!ID_PATTERN.test(id))
|
|
11
|
+
throw new Error(`Invalid prediction id "${raw}".`);
|
|
12
|
+
if (!unique.includes(id))
|
|
13
|
+
unique.push(id);
|
|
14
|
+
}
|
|
15
|
+
if (unique.length === 0 || unique.length > MAX_DELETE_IDS) {
|
|
16
|
+
throw new Error(`Pass between 1 and ${MAX_DELETE_IDS} prediction ids.`);
|
|
17
|
+
}
|
|
18
|
+
if (!confirm) {
|
|
19
|
+
throw new Error(`Nothing was deleted. This removes ${unique.length} prediction(s) from the history and cannot be undone here: ask the user, then call again with confirm: true.`);
|
|
20
|
+
}
|
|
21
|
+
return unique;
|
|
22
|
+
}
|
|
23
|
+
export function summarizeDelete(requested, deletedIds) {
|
|
24
|
+
const deleted = deletedIds ?? [];
|
|
25
|
+
const notDeleted = requested.filter((id) => !deleted.includes(id));
|
|
26
|
+
return {
|
|
27
|
+
deleted,
|
|
28
|
+
deleted_count: deleted.length,
|
|
29
|
+
requested: requested.length,
|
|
30
|
+
...(notDeleted.length > 0
|
|
31
|
+
? { not_deleted: notDeleted, note: "These ids were not deleted: they are not in this account's history, are still running, or were already deleted." }
|
|
32
|
+
: {}),
|
|
33
|
+
};
|
|
34
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -9,17 +9,19 @@
|
|
|
9
9
|
// * Inputs are never mutated. The one explicit transform is the `@path`
|
|
10
10
|
// marker, which uploads the referenced local file and substitutes its
|
|
11
11
|
// hosted URL. Bare paths pass through untouched.
|
|
12
|
-
// * Price quotes
|
|
13
|
-
//
|
|
12
|
+
// * Price quotes are estimates for the inputs given; the model's llms.txt
|
|
13
|
+
// is linked next to them for what each input means.
|
|
14
14
|
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, fetchHistory, fetchBillings, fetchUsageStats, PREDICTION_STATUSES, fetchPricing, submitPrediction, waitForPrediction, MCP_CLIENT_NAME, TERMINAL_FAILURES, } from './lib/api.js';
|
|
18
|
+
import { fetchModels, fetchBalance, fetchHistory, fetchBillings, fetchUsageStats, deletePredictions, PREDICTION_STATUSES, fetchPricing, modelLlmsTxtUrl, submitPrediction, waitForPrediction, MCP_CLIENT_NAME, TERMINAL_FAILURES, } from './lib/api.js';
|
|
19
19
|
import { getApiKey, getBaseUrl } from './lib/config.js';
|
|
20
|
+
import { MAX_DELETE_IDS, prepareDelete, summarizeDelete } from './lib/delete-predictions.js';
|
|
21
|
+
// Where to add credit; handed to the assistant next to the balance.
|
|
22
|
+
const TOP_UP_URL = 'https://wavespeed.ai/top-up';
|
|
20
23
|
import { resolveLocalFiles } from './lib/local-files.js';
|
|
21
24
|
import { uploadWithCache } from './lib/upload-cache.js';
|
|
22
|
-
import { missingPriceVars, isFloorQuote } from './lib/pricing-vars.js';
|
|
23
25
|
import { findUnknownInputs } from './lib/validate-inputs.js';
|
|
24
26
|
const require = createRequire(import.meta.url);
|
|
25
27
|
const { version: PKG_VERSION } = require('../package.json');
|
|
@@ -161,6 +163,7 @@ export function createServer() {
|
|
|
161
163
|
required: schema?.required ?? [],
|
|
162
164
|
properties: schema?.properties ?? {},
|
|
163
165
|
property_order: schema?.['x-order-properties'] ?? [],
|
|
166
|
+
llms_txt: modelLlmsTxtUrl(meta.model_id),
|
|
164
167
|
});
|
|
165
168
|
});
|
|
166
169
|
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 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.', {
|
|
@@ -205,43 +208,23 @@ export function createServer() {
|
|
|
205
208
|
...(done ? {} : { next: STILL_RUNNING_HINT }),
|
|
206
209
|
});
|
|
207
210
|
});
|
|
208
|
-
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.', {
|
|
211
|
+
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, and a quote made without them can be lower than the real charge.', {
|
|
209
212
|
model: z.string().describe('Model ID from list_models'),
|
|
210
213
|
input: z.record(z.unknown()).default({}).describe('Inputs the quote should account for'),
|
|
211
214
|
}, { readOnlyHint: false, destructiveHint: false, openWorldHint: true }, async ({ model, input }) => {
|
|
212
|
-
// Resolve @path markers exactly like run_model:
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
215
|
+
// Resolve @path markers exactly like run_model: some prices are measured
|
|
216
|
+
// from the file itself, so quoting with the literal "@./a.mp3" string
|
|
217
|
+
// would be wrong. The 24h content-hash cache means the later run reuses
|
|
218
|
+
// this upload.
|
|
216
219
|
const resolved = await resolveLocalFiles(input, {
|
|
217
220
|
upload: async (p) => (await uploadFile(p)).url,
|
|
218
221
|
});
|
|
219
222
|
const priceInput = resolved.input;
|
|
220
223
|
const data = await fetchPricing(model, priceInput);
|
|
221
|
-
|
|
222
|
-
// formula whose variables are all missing collapses to base_price, the
|
|
223
|
-
// floor of the model's range. Name what the quote could not see.
|
|
224
|
-
let unpriced = [];
|
|
225
|
-
let atFloor = false;
|
|
226
|
-
try {
|
|
227
|
-
const models = await fetchModels();
|
|
228
|
-
const meta = models.find((m) => m.model_id === model);
|
|
229
|
-
unpriced = missingPriceVars(meta?.formula, priceInput, requestSchema(meta ?? {})?.properties);
|
|
230
|
-
atFloor = isFloorQuote(meta?.formula, priceInput);
|
|
231
|
-
}
|
|
232
|
-
catch {
|
|
233
|
-
/* no catalog — the generic disclaimer still applies */
|
|
234
|
-
}
|
|
235
|
-
return ok({
|
|
236
|
-
...data,
|
|
237
|
-
estimate: true,
|
|
238
|
-
unpriced_inputs: unpriced,
|
|
239
|
-
at_base_price: atFloor,
|
|
240
|
-
disclaimer: PRICE_DISCLAIMER,
|
|
241
|
-
});
|
|
224
|
+
return ok({ ...data, estimate: true, disclaimer: PRICE_DISCLAIMER, llms_txt: modelLlmsTxtUrl(model) });
|
|
242
225
|
});
|
|
243
|
-
server.tool('get_balance', 'Show the WaveSpeed account credit balance.', {}, { readOnlyHint: true, openWorldHint: true }, async () => {
|
|
244
|
-
return ok(await fetchBalance());
|
|
226
|
+
server.tool('get_balance', 'Show the WaveSpeed account credit balance, with the link to top up.', {}, { readOnlyHint: true, openWorldHint: true }, async () => {
|
|
227
|
+
return ok({ ...(await fetchBalance()), top_up_url: TOP_UP_URL });
|
|
245
228
|
});
|
|
246
229
|
server.tool('list_predictions', 'List past predictions (run history), newest window first. Defaults to the last 24 hours; pass since/until (ISO or YYYY-MM-DD) for other ranges. Use get_prediction with an id for one task in full.', {
|
|
247
230
|
page: z.number().int().min(1).default(1),
|
|
@@ -288,6 +271,14 @@ export function createServer() {
|
|
|
288
271
|
const start = toIso(since, 'since') ?? new Date(Date.parse(end) - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
289
272
|
return ok(await fetchUsageStats(start, end, model));
|
|
290
273
|
});
|
|
274
|
+
server.tool('delete_predictions', `Delete finished predictions from this account's run history, up to ${MAX_DELETE_IDS} at a time. They stop appearing in list_predictions and get_prediction, and this tool cannot bring them back. Runs that are still going are left alone, and only the key's own organization's records are deleted. Ask the user first, and pass confirm: true only after they agreed to delete these ids.`, {
|
|
275
|
+
ids: z.array(z.string()).min(1).max(MAX_DELETE_IDS).describe('Prediction ids to delete'),
|
|
276
|
+
confirm: z.boolean().describe('Must be true: the user has agreed to delete these predictions'),
|
|
277
|
+
}, { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }, async ({ ids, confirm }) => {
|
|
278
|
+
const toDelete = prepareDelete(ids, confirm);
|
|
279
|
+
const res = await deletePredictions(toDelete);
|
|
280
|
+
return ok(summarizeDelete(toDelete, res.deleted_ids));
|
|
281
|
+
});
|
|
291
282
|
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 }) => {
|
|
292
283
|
return ok(await uploadFile(filePath));
|
|
293
284
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wavespeed/mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.15",
|
|
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": {
|
package/dist/lib/pricing-vars.js
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
// Pricing formulas are JSONata expressions returned by /api/v3/models, e.g.
|
|
2
|
-
// {"total_price": $max([5, $min([$ceil(get_duration_v3(audio)), 600])]) / 5 * base_price}
|
|
3
|
-
// {"total_price": base_price * duration / 5}
|
|
4
|
-
//
|
|
5
|
-
// The pricing endpoint evaluates them server-side and happily accepts a partial
|
|
6
|
-
// (or empty) `inputs` object — a formula whose variables are all missing
|
|
7
|
-
// collapses to `base_price`, which is the floor of the model's price range, not
|
|
8
|
-
// a typical charge. `wavespeed price wavespeed-ai/infinitetalk` with no inputs
|
|
9
|
-
// quotes $0.15 while a 60s audio actually costs $1.80. So we parse the formula
|
|
10
|
-
// to find which inputs move the price, and say which ones the quote is blind to.
|
|
11
|
-
const RESERVED = new Set(['base_price', 'total_price', 'true', 'false', 'null']);
|
|
12
|
-
/**
|
|
13
|
-
* Variable names a pricing formula reads from the model's inputs, in first-seen
|
|
14
|
-
* order. Excludes JSONata builtins (`$max`), helper calls (`get_duration_v3`),
|
|
15
|
-
* string literals, and `base_price`/`total_price`.
|
|
16
|
-
*
|
|
17
|
-
* Returns [] for a flat-priced model (`base_price * 1`) or an unparseable
|
|
18
|
-
* formula — callers should treat [] as "nothing input-dependent to warn about".
|
|
19
|
-
*/
|
|
20
|
-
export function extractPriceVars(formula) {
|
|
21
|
-
if (!formula)
|
|
22
|
-
return [];
|
|
23
|
-
// Drop string literals first so "720p" / "total_price" keys never register.
|
|
24
|
-
const stripped = formula.replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)*'/g, "''");
|
|
25
|
-
const vars = [];
|
|
26
|
-
const seen = new Set();
|
|
27
|
-
const re = /(\$?)([A-Za-z_][A-Za-z0-9_]*)\s*(\()?/g;
|
|
28
|
-
let m;
|
|
29
|
-
while ((m = re.exec(stripped)) !== null) {
|
|
30
|
-
const [, dollar, name, call] = m;
|
|
31
|
-
if (dollar)
|
|
32
|
-
continue; // $max, $ceil, $length — JSONata builtin
|
|
33
|
-
if (call)
|
|
34
|
-
continue; // get_duration_v3(...) — server-side helper
|
|
35
|
-
if (RESERVED.has(name))
|
|
36
|
-
continue;
|
|
37
|
-
if (seen.has(name))
|
|
38
|
-
continue;
|
|
39
|
-
seen.add(name);
|
|
40
|
-
vars.push(name);
|
|
41
|
-
}
|
|
42
|
-
return vars;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Price-driving variables the quote is genuinely blind to.
|
|
46
|
-
*
|
|
47
|
-
* A driver the caller omitted but that carries a schema default is NOT blind —
|
|
48
|
-
* the server priced it at that default, which is exactly what a run would use.
|
|
49
|
-
* Only drivers with no default (infinitetalk's `audio`, seedance's
|
|
50
|
-
* `reference_videos`) leave the quote uninformed, and those are the ones that
|
|
51
|
-
* silently collapse the formula toward base_price.
|
|
52
|
-
*/
|
|
53
|
-
export function missingPriceVars(formula, inputs, schemaProps) {
|
|
54
|
-
return extractPriceVars(formula).filter((v) => {
|
|
55
|
-
const val = inputs[v];
|
|
56
|
-
if (val !== undefined && val !== null && val !== '')
|
|
57
|
-
return false;
|
|
58
|
-
const prop = schemaProps?.[v];
|
|
59
|
-
return !(prop && 'default' in prop);
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* True when every price driver is unsupplied — the case where the formula
|
|
64
|
-
* degenerates to `base_price` and the quote is the floor of the model's range
|
|
65
|
-
* rather than a representative charge.
|
|
66
|
-
*/
|
|
67
|
-
export function isFloorQuote(formula, inputs) {
|
|
68
|
-
const vars = extractPriceVars(formula);
|
|
69
|
-
if (vars.length === 0)
|
|
70
|
-
return false;
|
|
71
|
-
return vars.every((v) => {
|
|
72
|
-
const val = inputs[v];
|
|
73
|
-
return val === undefined || val === null || val === '';
|
|
74
|
-
});
|
|
75
|
-
}
|