@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 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
- - **Honest pricing.** `get_price` quotes before you spend and names the inputs the quote was blind to (`unpriced_inputs`) instead of presenting a formula's floor as "the" price.
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 `unpriced_inputs` / `at_base_price` disclosure |
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 name the inputs they were blind to instead of presenting
13
- // the formula's floor as "the" price.
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: pricing formulas often
213
- // read the file itself (get_duration_v3(audio)), so quoting with the
214
- // literal "@./a.mp3" string would silently collapse to the floor. The
215
- // 24h content-hash cache means the later run reuses this upload.
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
- // The pricing endpoint accepts partial inputs without complaint — a
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.13",
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": {
@@ -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
- }