@wavespeed/mcp 1.0.14 → 1.0.16

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,16 +25,16 @@ 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 docs page |
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 |
37
+ | `get_price` | Cost estimate for the inputs you pass (no charge), with a link to the model's docs page |
38
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) |
package/dist/lib/api.js CHANGED
@@ -200,6 +200,13 @@ 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's page on the docs site (same rule as the dashboard's model page
204
+ // link): provider folder, then the id without a leading "wavespeed-ai/" and
205
+ // with "/" turned into "-".
206
+ export function modelDocsUrl(modelId) {
207
+ const provider = modelId.split('/')[0];
208
+ return `https://wavespeed.ai/docs/docs-api/${provider}/${modelId.replace('wavespeed-ai/', '').split('/').join('-')}`;
209
+ }
203
210
  export async function fetchPricing(modelId, inputs) {
204
211
  // /model/price is the documented pricing endpoint.
205
212
  return apiPost('/api/v3/model/price', { model_id: modelId, inputs });
package/dist/server.js CHANGED
@@ -9,20 +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 docs page
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, deletePredictions, 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, modelDocsUrl, submitPrediction, waitForPrediction, MCP_CLIENT_NAME, TERMINAL_FAILURES, } from './lib/api.js';
19
19
  import { getApiKey, getBaseUrl } from './lib/config.js';
20
20
  import { MAX_DELETE_IDS, prepareDelete, summarizeDelete } from './lib/delete-predictions.js';
21
21
  // Where to add credit; handed to the assistant next to the balance.
22
22
  const TOP_UP_URL = 'https://wavespeed.ai/top-up';
23
23
  import { resolveLocalFiles } from './lib/local-files.js';
24
24
  import { uploadWithCache } from './lib/upload-cache.js';
25
- import { missingPriceVars, isFloorQuote } from './lib/pricing-vars.js';
26
25
  import { findUnknownInputs } from './lib/validate-inputs.js';
27
26
  const require = createRequire(import.meta.url);
28
27
  const { version: PKG_VERSION } = require('../package.json');
@@ -164,6 +163,7 @@ export function createServer() {
164
163
  required: schema?.required ?? [],
165
164
  properties: schema?.properties ?? {},
166
165
  property_order: schema?.['x-order-properties'] ?? [],
166
+ docs_url: modelDocsUrl(meta.model_id),
167
167
  });
168
168
  });
169
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.', {
@@ -208,40 +208,20 @@ export function createServer() {
208
208
  ...(done ? {} : { next: STILL_RUNNING_HINT }),
209
209
  });
210
210
  });
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.', {
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.', {
212
212
  model: z.string().describe('Model ID from list_models'),
213
213
  input: z.record(z.unknown()).default({}).describe('Inputs the quote should account for'),
214
214
  }, { readOnlyHint: false, destructiveHint: false, openWorldHint: true }, async ({ model, input }) => {
215
- // Resolve @path markers exactly like run_model: pricing formulas often
216
- // read the file itself (get_duration_v3(audio)), so quoting with the
217
- // literal "@./a.mp3" string would silently collapse to the floor. The
218
- // 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.
219
219
  const resolved = await resolveLocalFiles(input, {
220
220
  upload: async (p) => (await uploadFile(p)).url,
221
221
  });
222
222
  const priceInput = resolved.input;
223
223
  const data = await fetchPricing(model, priceInput);
224
- // The pricing endpoint accepts partial inputs without complaint — a
225
- // formula whose variables are all missing collapses to base_price, the
226
- // floor of the model's range. Name what the quote could not see.
227
- let unpriced = [];
228
- let atFloor = false;
229
- try {
230
- const models = await fetchModels();
231
- const meta = models.find((m) => m.model_id === model);
232
- unpriced = missingPriceVars(meta?.formula, priceInput, requestSchema(meta ?? {})?.properties);
233
- atFloor = isFloorQuote(meta?.formula, priceInput);
234
- }
235
- catch {
236
- /* no catalog — the generic disclaimer still applies */
237
- }
238
- return ok({
239
- ...data,
240
- estimate: true,
241
- unpriced_inputs: unpriced,
242
- at_base_price: atFloor,
243
- disclaimer: PRICE_DISCLAIMER,
244
- });
224
+ return ok({ ...data, estimate: true, disclaimer: PRICE_DISCLAIMER, docs_url: modelDocsUrl(model) });
245
225
  });
246
226
  server.tool('get_balance', 'Show the WaveSpeed account credit balance, with the link to top up.', {}, { readOnlyHint: true, openWorldHint: true }, async () => {
247
227
  return ok({ ...(await fetchBalance()), top_up_url: TOP_UP_URL });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wavespeed/mcp",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
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
- }