@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 +3 -3
- package/dist/lib/api.js +7 -0
- package/dist/server.js +10 -30
- package/package.json +1 -1
- package/dist/lib/pricing-vars.js +0 -75
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
|
-
- **
|
|
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
|
|
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
|
|
13
|
-
//
|
|
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:
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
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
|
-
|
|
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.
|
|
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": {
|
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
|
-
}
|