@wavespeed/mcp 1.2.3 → 1.2.5

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.
@@ -300,13 +300,29 @@
300
300
  return said;
301
301
  }
302
302
 
303
+ // Ask the server for the run through the host: the standard bridge first (tools/call), then ChatGPT's own
304
+ // window.openai.callTool when the standard call is refused. Both answer with the tool result (ChatGPT's may come back
305
+ // as {structuredContent}, {content} or {result: "<json text>"}).
306
+ function callServerTool(name, toolArgs) {
307
+ return request("tools/call", { name: name, arguments: toolArgs }).catch(function (err) {
308
+ var o = window.openai;
309
+ if (!o || typeof o.callTool !== "function" || (err && err.message === "timeout")) throw err;
310
+ return Promise.resolve(o.callTool(name, toolArgs)).then(function (r) {
311
+ if (r && typeof r.result === "string" && !r.structuredContent && !r.content) {
312
+ try { return { structuredContent: JSON.parse(r.result) }; } catch (e) { return { content: [{ type: "text", text: r.result }] }; }
313
+ }
314
+ return r;
315
+ }, function () { throw err; });
316
+ });
317
+ }
318
+
303
319
  // Keep asking the server while the run is going: this is what turns a video's wait into a card that fills in.
304
320
  function poll(id, token) {
305
321
  var asked = 0;
306
322
  (function again() {
307
323
  if (token !== pollToken) return;
308
324
  asked++;
309
- request("tools/call", { name: "get_prediction", arguments: { id: id, wait_seconds: 25 } }).then(function (res) {
325
+ callServerTool("get_prediction", { id: id, wait_seconds: 25 }).then(function (res) {
310
326
  if (token !== pollToken) return;
311
327
  var d = res && res.structuredContent;
312
328
  if (!d) {
package/dist/lib/app.js CHANGED
@@ -3,12 +3,14 @@
3
3
  // the metadata and show the text as before. The view is the same file the
4
4
  // remote server (api-server /mcp) serves; keep the two copies identical.
5
5
  import { readFileSync } from 'node:fs';
6
- export const RESULT_VIEW_URI = 'ui://wavespeed/result.html';
6
+ // A host keeps a UI resource by its URI, so a changed card only reaches it under a new URI: bump the number.
7
+ export const RESULT_VIEW_URI = 'ui://wavespeed/result-v2.html';
7
8
  export const APP_MIME_TYPE = 'text/html;profile=mcp-app';
8
9
  export const TOP_UP_URL = 'https://wavespeed.ai/top-up';
10
+ // get_prediction is not one of them: the card asks it for itself while the run goes on, and an assistant that polls it
11
+ // would draw a second card for the same result. It is marked callable from the card instead (see toolMeta).
9
12
  export const UI_TOOLS = new Set([
10
13
  'run_model',
11
- 'get_prediction',
12
14
  'generate_image',
13
15
  'edit_image',
14
16
  'generate_video',
@@ -18,6 +20,38 @@ export const UI_TOOLS = new Set([
18
20
  // "https:" alone should allow every https source, but a host may only honour named hosts when it builds the
19
21
  // card's CSP, so the usual media hosts are named too, as wildcards.
20
22
  export const VIEW_RESOURCE_DOMAINS = ['https:', 'https://*.cloudfront.net', 'https://*.wavespeed.ai', 'https://*.amazonaws.com'];
23
+ // The one-line status ChatGPT shows while a tool runs and after it ran (it allows 64 characters).
24
+ const TOOL_STATUS = {
25
+ run_model: ['Starting the run…', 'Run started'],
26
+ generate_image: ['Starting the run…', 'Run started'],
27
+ edit_image: ['Starting the run…', 'Run started'],
28
+ generate_video: ['Starting the run…', 'Run started'],
29
+ edit_video: ['Starting the run…', 'Run started'],
30
+ extend_video: ['Starting the run…', 'Run started'],
31
+ get_prediction: ['Checking the run…', 'Run checked'],
32
+ };
33
+ /**
34
+ * The tool's _meta for the card: the run tools open it; get_prediction does not but says the card may call it
35
+ * (ChatGPT only lets a widget call a tool marked openai/widgetAccessible; MCP Apps hosts read ui.visibility, whose
36
+ * default already includes "app"). Same as the remote server (api-server mcp_app.go).
37
+ */
38
+ export function toolMeta(name) {
39
+ const linked = UI_TOOLS.has(name);
40
+ if (!linked && name !== 'get_prediction')
41
+ return undefined;
42
+ const meta = {
43
+ ui: linked ? { resourceUri: RESULT_VIEW_URI, visibility: ['model', 'app'] } : { visibility: ['model', 'app'] },
44
+ 'openai/widgetAccessible': true,
45
+ };
46
+ if (linked)
47
+ meta['openai/outputTemplate'] = RESULT_VIEW_URI;
48
+ const status = TOOL_STATUS[name];
49
+ if (status) {
50
+ meta['openai/toolInvocation/invoking'] = status[0];
51
+ meta['openai/toolInvocation/invoked'] = status[1];
52
+ }
53
+ return meta;
54
+ }
21
55
  export function loadView() {
22
56
  return readFileSync(new URL('../../assets/result.html', import.meta.url), 'utf8');
23
57
  }
package/dist/server.js CHANGED
@@ -24,7 +24,7 @@ import { MAX_DELETE_IDS, prepareDelete, summarizeDelete } from './lib/delete-pre
24
24
  // Where to add credit; handed to the assistant next to the balance.
25
25
  const TOP_UP_URL = 'https://wavespeed.ai/top-up';
26
26
  import { resolveLocalFiles } from './lib/local-files.js';
27
- import { APP_MIME_TYPE, RESULT_VIEW_URI, UI_TOOLS, VIEW_RESOURCE_DOMAINS, insufficientCreditsResult, isInsufficientCredits, loadView, } from './lib/app.js';
27
+ import { APP_MIME_TYPE, RESULT_VIEW_URI, toolMeta, VIEW_RESOURCE_DOMAINS, insufficientCreditsResult, isInsufficientCredits, loadView, } from './lib/app.js';
28
28
  import { uploadWithCache } from './lib/upload-cache.js';
29
29
  import { findUnknownInputs } from './lib/validate-inputs.js';
30
30
  import { IMAGE_MODEL_KEYS, VIDEO_MODEL_KEYS } from './lib/media-models.js';
@@ -203,8 +203,9 @@ export function createServer() {
203
203
  }
204
204
  };
205
205
  const tool = registerTool(...args);
206
- if (UI_TOOLS.has(name))
207
- tool.update({ _meta: { ui: { resourceUri: RESULT_VIEW_URI } } });
206
+ const meta = toolMeta(name);
207
+ if (meta)
208
+ tool.update({ _meta: meta });
208
209
  return tool;
209
210
  };
210
211
  server.registerResource('result-card', RESULT_VIEW_URI, {
@@ -217,7 +218,13 @@ export function createServer() {
217
218
  uri: uri.href,
218
219
  mimeType: APP_MIME_TYPE,
219
220
  text: loadView(),
220
- _meta: { ui: { csp: { resourceDomains: VIEW_RESOURCE_DOMAINS, connectDomains: [] }, prefersBorder: true } },
221
+ _meta: {
222
+ ui: { csp: { resourceDomains: VIEW_RESOURCE_DOMAINS, connectDomains: [] }, prefersBorder: true },
223
+ // The same, in the names ChatGPT's own extensions use, for versions that read only those.
224
+ 'openai/widgetCSP': { resource_domains: VIEW_RESOURCE_DOMAINS, connect_domains: [] },
225
+ 'openai/widgetPrefersBorder': true,
226
+ 'openai/widgetDescription': 'Shows the images, videos and audio of a WaveSpeed run and follows it until it finishes.',
227
+ },
221
228
  },
222
229
  ],
223
230
  }));
@@ -229,7 +236,7 @@ export function createServer() {
229
236
  .describe('Filter by modality type, e.g. text-to-image, image-to-video'),
230
237
  limit: z.number().int().min(1).max(200).default(30).describe('Max results'),
231
238
  refresh: z.boolean().default(false).describe('Bypass the 1h catalog cache'),
232
- }, { readOnlyHint: true, openWorldHint: true }, async ({ query, type, limit, refresh }) => {
239
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }, async ({ query, type, limit, refresh }) => {
233
240
  const models = await fetchModels({ refresh });
234
241
  const q = query?.toLowerCase();
235
242
  const filtered = models.filter((m) => {
@@ -246,7 +253,7 @@ export function createServer() {
246
253
  models: filtered.slice(0, limit).map(compactModel),
247
254
  });
248
255
  });
249
- server.tool('get_model_schema', "Get a model's real input schema (required fields, properties, defaults). Call this before run_model so inputs match what the model actually accepts.", { model: z.string().describe('Model ID from list_models, e.g. bytedance/seedream-v5.0-pro') }, { readOnlyHint: true, openWorldHint: true }, async ({ model }) => {
256
+ server.tool('get_model_schema', "Get a model's real input schema (required fields, properties, defaults). Call this before run_model so inputs match what the model actually accepts.", { model: z.string().describe('Model ID from list_models, e.g. bytedance/seedream-v5.0-pro') }, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }, async ({ model }) => {
250
257
  const models = await fetchModels();
251
258
  const meta = models.find((m) => m.model_id === model);
252
259
  if (!meta) {
@@ -436,7 +443,7 @@ export function createServer() {
436
443
  const data = await fetchPricing(model, priceInput);
437
444
  return ok({ ...data, estimate: true, disclaimer: PRICE_DISCLAIMER, playground_url: modelPlaygroundUrl(model) });
438
445
  });
439
- server.tool('get_balance', 'Show the WaveSpeed account credit balance, with the link to top up.', {}, { readOnlyHint: true, openWorldHint: true }, async () => {
446
+ server.tool('get_balance', 'Show the WaveSpeed account credit balance, with the link to top up.', {}, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }, async () => {
440
447
  return ok({ ...(await fetchBalance()), top_up_url: TOP_UP_URL });
441
448
  });
442
449
  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.', {
@@ -446,7 +453,7 @@ export function createServer() {
446
453
  status: z.enum(PREDICTION_STATUSES).optional(),
447
454
  since: z.string().optional().describe('Created after this time (ISO or YYYY-MM-DD)'),
448
455
  until: z.string().optional().describe('Created before this time (ISO or YYYY-MM-DD)'),
449
- }, { readOnlyHint: true, openWorldHint: true }, async ({ page, limit, model, status, since, until }) => {
456
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }, async ({ page, limit, model, status, since, until }) => {
450
457
  return ok(await fetchHistory({
451
458
  page,
452
459
  pageSize: limit,
@@ -464,7 +471,7 @@ export function createServer() {
464
471
  key: z.string().optional().describe('Filter by the API key uuid that generated the charge'),
465
472
  since: z.string().optional().describe('Records after this time (ISO or YYYY-MM-DD)'),
466
473
  until: z.string().optional().describe('Records before this time (ISO or YYYY-MM-DD)'),
467
- }, { readOnlyHint: true, openWorldHint: true }, async ({ page, limit, type, model, key, since, until }) => {
474
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }, async ({ page, limit, type, model, key, since, until }) => {
468
475
  return ok(await fetchBillings({
469
476
  page,
470
477
  pageSize: limit,
@@ -479,7 +486,7 @@ export function createServer() {
479
486
  since: z.string().optional().describe('Start (ISO or YYYY-MM-DD); default 7 days ago'),
480
487
  until: z.string().optional().describe('End (ISO or YYYY-MM-DD); default now'),
481
488
  model: z.string().optional().describe('Filter by model uuid'),
482
- }, { readOnlyHint: true, openWorldHint: true }, async ({ since, until, model }) => {
489
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }, async ({ since, until, model }) => {
483
490
  const end = toIso(until, 'until') ?? new Date().toISOString();
484
491
  const start = toIso(since, 'since') ?? new Date(Date.parse(end) - 7 * 24 * 60 * 60 * 1000).toISOString();
485
492
  return ok(await fetchUsageStats(start, end, model));
@@ -504,7 +511,7 @@ export function createServer() {
504
511
  .max(1800)
505
512
  .default(0)
506
513
  .describe('Max seconds to wait for a terminal status; 0 = return the current status now'),
507
- }, { readOnlyHint: true, openWorldHint: true }, async ({ id, wait_seconds }, extra) => {
514
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }, async ({ id, wait_seconds }, extra) => {
508
515
  const started = Date.now();
509
516
  const { item, done } = await waitForPrediction(id, {
510
517
  intervalMs: POLL_INTERVAL_MS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wavespeed/mcp",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
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": {