@wavespeed/mcp 1.0.11 → 1.0.13

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
@@ -36,10 +36,13 @@ Built on the same design as the open-source [`@wavespeed/cli`](https://github.co
36
36
  | `run_model` | Submit + wait; rejects input keys the model doesn't accept; `@path` inputs upload automatically; returns output URLs |
37
37
  | `get_price` | Cost estimate with `unpriced_inputs` / `at_base_price` disclosure |
38
38
  | `get_balance` | Account credit balance |
39
+ | `list_predictions` | Run history (default last 24h); filter by model, status, time range |
40
+ | `list_billings` | Itemized charges and refunds (needs a Billing-access key) |
41
+ | `get_usage` | Spend, request counts and per-model breakdown (default last 7 days; needs a Billing-access key) |
39
42
  | `upload_file` | Local file → hosted URL (24h content-hash dedupe) |
40
43
  | `get_prediction` | Status/outputs of any run by id; optional `wait_seconds` to wait for it |
41
44
 
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.
45
+ Some generations take minutes to hours. `run_model` submits the run and returns the prediction id right away (`done: false`); the assistant then calls `get_prediction` with that id and `wait_seconds` (max 1800) until `done` is `true`. The task keeps running server-side, so a dropped connection loses nothing. Pass `wait_seconds` to `run_model` to wait for a quick model in the same call. While waiting, both tools send MCP progress notifications that carry the prediction id.
43
46
 
44
47
  ## Setup
45
48
 
@@ -63,7 +66,7 @@ https://api.wavespeed.ai/mcp
63
66
  - **VS Code:** `code --add-mcp '{"name":"wavespeed","type":"http","url":"https://api.wavespeed.ai/mcp"}'`
64
67
  - **Any other client that sends request headers:** point it at the URL with `Authorization: Bearer <your API key>`.
65
68
 
66
- The hosted server has the same tools except for files: it cannot read files on your computer, so pass public URLs, or use `create_upload` from a tool that can send HTTP requests (it returns a temporary upload address and the `download_url` to use as an input). Full guide: [wavespeed.ai/docs/mcp](https://wavespeed.ai/docs/mcp).
69
+ The hosted server has the same tools, with one difference for files: it cannot read files on your computer, so pass public URLs, use `create_upload` from a tool that can send HTTP requests (it returns a temporary upload address and the `download_url` to use as an input), or, in ChatGPT, attach the file to the chat (the `upload_attachment` tool, which only ChatGPT can fill, imports it and returns a `download_url`). Full guide: [wavespeed.ai/docs/mcp](https://wavespeed.ai/docs/mcp).
67
70
 
68
71
  ### Local (npx)
69
72
 
package/dist/lib/api.js CHANGED
@@ -162,6 +162,44 @@ export async function fetchModels(opts = {}) {
162
162
  export async function fetchBalance() {
163
163
  return apiGet('/api/v3/balance');
164
164
  }
165
+ export const PREDICTION_STATUSES = ['created', 'processing', 'completed', 'failed', 'cancelled', 'timeout', 'deleted'];
166
+ // Same contract as wavespeed-cli `history`: the endpoint needs a time window,
167
+ // so default to the last 24 hours.
168
+ export async function fetchHistory(query = {}) {
169
+ const now = new Date();
170
+ const body = {
171
+ page: query.page ?? 1,
172
+ page_size: query.pageSize ?? 20,
173
+ created_after: query.createdAfter ?? new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(),
174
+ created_before: query.createdBefore ?? now.toISOString(),
175
+ };
176
+ if (query.model)
177
+ body.model = query.model;
178
+ if (query.status)
179
+ body.status = query.status;
180
+ return apiPost('/api/v3/predictions', body, { timeoutMs: GET_TIMEOUT_MS });
181
+ }
182
+ // Needs a key with Billing access; otherwise the platform's own error is surfaced.
183
+ export async function fetchBillings(query = {}) {
184
+ const body = { page: query.page ?? 1, page_size: query.pageSize ?? 20 };
185
+ if (query.type)
186
+ body.billing_type = query.type;
187
+ if (query.startTime)
188
+ body.start_time = query.startTime;
189
+ if (query.endTime)
190
+ body.end_time = query.endTime;
191
+ if (query.model)
192
+ body.model_uuid = query.model;
193
+ if (query.accessKey)
194
+ body.access_key_uuid = query.accessKey;
195
+ return apiPost('/api/v3/billings/search', body, { timeoutMs: GET_TIMEOUT_MS });
196
+ }
197
+ export async function fetchUsageStats(startTime, endTime, model) {
198
+ const body = { start_time: startTime, end_time: endTime };
199
+ if (model)
200
+ body.model_uuid = model;
201
+ return apiPost('/api/v3/user/usage_stats', body, { timeoutMs: GET_TIMEOUT_MS });
202
+ }
165
203
  export async function fetchPricing(modelId, inputs) {
166
204
  // /model/price is the documented pricing endpoint.
167
205
  return apiPost('/api/v3/model/price', { model_id: modelId, inputs });
package/dist/server.js CHANGED
@@ -15,7 +15,7 @@ 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, submitPrediction, waitForPrediction, MCP_CLIENT_NAME, TERMINAL_FAILURES, } from './lib/api.js';
18
+ import { fetchModels, fetchBalance, fetchHistory, fetchBillings, fetchUsageStats, PREDICTION_STATUSES, 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';
@@ -23,6 +23,14 @@ import { missingPriceVars, isFloorQuote } from './lib/pricing-vars.js';
23
23
  import { findUnknownInputs } from './lib/validate-inputs.js';
24
24
  const require = createRequire(import.meta.url);
25
25
  const { version: PKG_VERSION } = require('../package.json');
26
+ function toIso(value, name) {
27
+ if (!value)
28
+ return undefined;
29
+ const t = Date.parse(value);
30
+ if (Number.isNaN(t))
31
+ throw new Error(`Invalid ${name} "${value}": use ISO 8601 or YYYY-MM-DD.`);
32
+ return new Date(t).toISOString();
33
+ }
26
34
  const PRICE_DISCLAIMER = 'Estimate only, for reference — the amount actually charged for a run is authoritative.';
27
35
  function requestSchema(m) {
28
36
  return m.api_schema?.api_schemas?.[0]?.request_schema;
@@ -43,13 +51,14 @@ async function uploadFile(filePath) {
43
51
  const client = new Client(apiKey, { baseUrl: getBaseUrl(), clientName: MCP_CLIENT_NAME });
44
52
  return uploadWithCache(filePath, (p) => client.upload(p));
45
53
  }
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;
54
+ // run_model submits and returns the prediction id right away (same as the
55
+ // hosted server); the assistant then waits with get_prediction. Many MCP
56
+ // clients abort a tool call after 60s and an aborted call never delivers the
57
+ // id, so a long wait is opt-in via wait_seconds.
58
+ const DEFAULT_WAIT_SECONDS = 0;
50
59
  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.';
60
+ const STILL_RUNNING_HINT = 'The run was submitted and is processing on the server. Call get_prediction with this id and wait_seconds ' +
61
+ 'until done=true; it returns the output URLs. Do not submit the same run again: a new run is billed again.';
53
62
  // Emits notifications/progress while polling when the client asked for it.
54
63
  // Each one carries the prediction id, and clients that reset their timeout
55
64
  // on progress keep a long wait alive.
@@ -154,7 +163,7 @@ export function createServer() {
154
163
  property_order: schema?.['x-order-properties'] ?? [],
155
164
  });
156
165
  });
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.', {
166
+ 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.', {
158
167
  model: z.string().describe('Model ID from list_models'),
159
168
  input: z
160
169
  .record(z.unknown())
@@ -165,7 +174,7 @@ export function createServer() {
165
174
  .min(0)
166
175
  .max(1800)
167
176
  .default(DEFAULT_WAIT_SECONDS)
168
- .describe(`Max seconds to wait before returning the still-running prediction id (default ${DEFAULT_WAIT_SECONDS}); 0 = submit only`),
177
+ .describe(`Max seconds to wait for the result in this call (default ${DEFAULT_WAIT_SECONDS}: return the prediction id right away)`),
169
178
  }, { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ model, input, wait_seconds }, extra) => {
170
179
  const resolved = await resolveLocalFiles(input, {
171
180
  upload: async (p) => (await uploadFile(p)).url,
@@ -234,6 +243,51 @@ export function createServer() {
234
243
  server.tool('get_balance', 'Show the WaveSpeed account credit balance.', {}, { readOnlyHint: true, openWorldHint: true }, async () => {
235
244
  return ok(await fetchBalance());
236
245
  });
246
+ 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
+ page: z.number().int().min(1).default(1),
248
+ limit: z.number().int().min(1).max(100).default(20).describe('Page size'),
249
+ model: z.string().optional().describe('Filter by model ID'),
250
+ status: z.enum(PREDICTION_STATUSES).optional(),
251
+ since: z.string().optional().describe('Created after this time (ISO or YYYY-MM-DD)'),
252
+ until: z.string().optional().describe('Created before this time (ISO or YYYY-MM-DD)'),
253
+ }, { readOnlyHint: true, openWorldHint: true }, async ({ page, limit, model, status, since, until }) => {
254
+ return ok(await fetchHistory({
255
+ page,
256
+ pageSize: limit,
257
+ model,
258
+ status,
259
+ createdAfter: toIso(since, 'since'),
260
+ createdBefore: toIso(until, 'until'),
261
+ }));
262
+ });
263
+ server.tool('list_billings', 'List itemized charges and refunds (billing records). Requires an API key with Billing access.', {
264
+ page: z.number().int().min(1).default(1),
265
+ limit: z.number().int().min(1).max(100).default(20).describe('Page size'),
266
+ type: z.enum(['deduct', 'refund']).optional(),
267
+ model: z.string().optional().describe('Filter by model uuid'),
268
+ key: z.string().optional().describe('Filter by the API key uuid that generated the charge'),
269
+ since: z.string().optional().describe('Records after this time (ISO or YYYY-MM-DD)'),
270
+ until: z.string().optional().describe('Records before this time (ISO or YYYY-MM-DD)'),
271
+ }, { readOnlyHint: true, openWorldHint: true }, async ({ page, limit, type, model, key, since, until }) => {
272
+ return ok(await fetchBillings({
273
+ page,
274
+ pageSize: limit,
275
+ type,
276
+ model,
277
+ accessKey: key,
278
+ startTime: toIso(since, 'since'),
279
+ endTime: toIso(until, 'until'),
280
+ }));
281
+ });
282
+ server.tool('get_usage', 'Usage statistics: spend, request counts, daily series and per-model breakdown. Defaults to the last 7 days. Requires an API key with Billing access.', {
283
+ since: z.string().optional().describe('Start (ISO or YYYY-MM-DD); default 7 days ago'),
284
+ until: z.string().optional().describe('End (ISO or YYYY-MM-DD); default now'),
285
+ model: z.string().optional().describe('Filter by model uuid'),
286
+ }, { readOnlyHint: true, openWorldHint: true }, async ({ since, until, model }) => {
287
+ const end = toIso(until, 'until') ?? new Date().toISOString();
288
+ const start = toIso(since, 'since') ?? new Date(Date.parse(end) - 7 * 24 * 60 * 60 * 1000).toISOString();
289
+ return ok(await fetchUsageStats(start, end, model));
290
+ });
237
291
  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 }) => {
238
292
  return ok(await uploadFile(filePath));
239
293
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wavespeed/mcp",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
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": {