@wavespeed/mcp 1.2.1 → 1.2.3

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.
@@ -18,9 +18,12 @@
18
18
  .prompt { color: var(--muted); margin: 0 0 10px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
19
19
  .grid { display: grid; gap: 8px; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); }
20
20
  .grid.one { grid-template-columns: 1fr; }
21
- .tile { background: var(--card); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
21
+ .tile { position: relative; background: var(--card); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
22
22
  .tile img, .tile video { display: block; width: 100%; max-height: 520px; object-fit: contain; background: var(--card); }
23
23
  .tile video { background: #000; }
24
+ .tile-spin { position: absolute; top: 40%; left: 50%; margin: -9px 0 0 -9px; pointer-events: none; }
25
+ .code { margin: 0; padding: 12px; max-height: 360px; overflow: auto; font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; word-break: break-word; }
26
+ .text { padding: 12px; white-space: pre-wrap; word-break: break-word; max-height: 360px; overflow: auto; }
24
27
  .broken { padding: 28px 12px; text-align: center; color: var(--muted); font-size: 13px; }
25
28
  .tile img { cursor: zoom-in; }
26
29
  .tile audio { display: block; width: 100%; padding: 10px; }
@@ -53,7 +56,7 @@
53
56
  // A small MCP Apps view (https://modelcontextprotocol.io/seps/1865-mcp-apps-interactive-user-interfaces-for-mcp):
54
57
  // it speaks JSON-RPC to the host over postMessage and shows what the tool returned.
55
58
  var PROTOCOL = "2026-01-26";
56
- var nextId = 1, pending = {}, args = {}, current = null, pollToken = 0, started = 0, ticker = null;
59
+ var nextId = 1, pending = {}, args = {}, current = null, pollToken = 0, started = 0, ticker = null, hostCaps = null;
57
60
  var $ = function (id) { return document.getElementById(id); };
58
61
 
59
62
  function send(msg) { try { window.parent.postMessage(msg, "*"); } catch (e) {} }
@@ -75,7 +78,11 @@
75
78
  var p = pending[m.id];
76
79
  if (!p) return;
77
80
  delete pending[m.id];
78
- if (m.error) p.reject(new Error(m.error.message || "error")); else p.resolve(m.result);
81
+ if (m.error) {
82
+ var err = new Error(m.error.message || "error");
83
+ err.code = m.error.code;
84
+ p.reject(err);
85
+ } else p.resolve(m.result);
79
86
  return;
80
87
  }
81
88
  if (m.method === "ui/notifications/tool-input") { args = (m.params && m.params.arguments) || {}; showPrompt(); }
@@ -113,19 +120,21 @@
113
120
  resize();
114
121
  }
115
122
 
116
- function urlOf(o) {
117
- if (typeof o === "string") return o;
118
- if (o && typeof o === "object") return o.url || o.download_url || o.href || "";
119
- return "";
120
- }
123
+ // File types by extension, the same lists the WaveSpeed site uses to pick a preview.
124
+ var EXT = {
125
+ image: "jpg jpeg png gif webp avif bmp tiff tif svg ico",
126
+ video: "mp4 mov avi mkv webm flv wmv m4v",
127
+ audio: "mp3 wav m4a ogg aac flac wma aiff au mid midi rmi opus",
128
+ model: "glb gltf",
129
+ subtitle: "srt vtt",
130
+ };
121
131
  function kindOf(url) {
122
132
  var path = url.split("?")[0].split("#")[0].toLowerCase();
123
133
  var ext = path.slice(path.lastIndexOf(".") + 1);
124
- if (/^(png|jpe?g|webp|gif|avif|bmp)$/.test(ext)) return "image";
125
- if (/^(mp4|webm|mov|m4v)$/.test(ext)) return "video";
126
- if (/^(mp3|wav|m4a|ogg|flac|aac)$/.test(ext)) return "audio";
134
+ for (var k in EXT) if ((" " + EXT[k] + " ").indexOf(" " + ext + " ") >= 0) return k;
127
135
  return "file";
128
136
  }
137
+ var KIND_LABEL = { model: "3D model", subtitle: "Subtitles", file: "File" };
129
138
  function el(tag, cls, text) {
130
139
  var n = document.createElement(tag);
131
140
  if (cls) n.className = cls;
@@ -138,30 +147,74 @@
138
147
  return b;
139
148
  }
140
149
 
141
- function tile(url) {
142
- var kind = kindOf(url), t = el("div", "tile"), media;
143
- if (!/^https?:\/\//i.test(url)) return null;
150
+ // Like the site: one thing plays at a time.
151
+ function onlyOnePlays(media) {
152
+ media.addEventListener("play", function () {
153
+ document.querySelectorAll("video, audio").forEach(function (m) { if (m !== media) m.pause(); });
154
+ });
155
+ }
156
+ // A spinner stands in until the media has something to draw (the site does the same).
157
+ function withSpinner(t, media, readyEvent) {
158
+ var sp = el("div", "spin tile-spin");
159
+ t.appendChild(sp);
160
+ var done = function () { if (sp.parentNode) sp.parentNode.removeChild(sp); };
161
+ media.addEventListener(readyEvent, done);
162
+ media.addEventListener("error", done);
163
+ }
164
+
165
+ function brokenNote(t, media, what) {
166
+ // The host may not let the card load this address; say so and keep the Open button working.
167
+ media.addEventListener("error", function () {
168
+ if (media.parentNode !== t) return;
169
+ t.replaceChild(el("div", "broken", "This chat can't show the " + what + " here. Use Open / save."), media); resize();
170
+ });
171
+ }
172
+
173
+ // One tile per output. Outputs are links to files, plain text, or JSON.
174
+ function tile(o) {
175
+ var t = el("div", "tile");
176
+ if (o !== null && typeof o === "object") {
177
+ var text = JSON.stringify(o, null, 2);
178
+ t.appendChild(el("pre", "code", text));
179
+ var jm = el("div", "meta"); jm.appendChild(el("span", "", "JSON"));
180
+ var cb = el("button", "", "Copy");
181
+ cb.addEventListener("click", function () {
182
+ try { navigator.clipboard.writeText(text).then(function () { cb.textContent = "Copied"; }, function () { cb.textContent = "Copy failed"; }); }
183
+ catch (e) { cb.textContent = "Copy failed"; }
184
+ });
185
+ jm.appendChild(cb); t.appendChild(jm);
186
+ return t;
187
+ }
188
+ var url = typeof o === "string" ? o : String(o);
189
+ if (!/^https?:\/\//i.test(url)) { t.appendChild(el("div", "text", url)); return t; }
190
+ var kind = kindOf(url), media;
144
191
  if (kind === "image") {
145
192
  media = el("img"); media.src = url; media.alt = "Generated image"; media.loading = "lazy";
146
193
  media.addEventListener("click", function () { openLink(url); });
147
194
  media.addEventListener("load", resize);
148
- // The host may not let the card load this address; say so and keep the Open button working.
149
- media.addEventListener("error", function () {
150
- var m = el("div", "broken", "This chat can't show the image here. Use Open / save.");
151
- t.replaceChild(m, media); resize();
152
- });
195
+ brokenNote(t, media, "image");
153
196
  t.appendChild(media);
197
+ withSpinner(t, media, "load");
154
198
  } else if (kind === "video") {
155
- media = el("video"); media.src = url; media.controls = true; media.playsInline = true; media.preload = "metadata";
199
+ // #t=0.001 makes the browser draw the first frame; without it iOS shows a black box until play.
200
+ media = el("video"); media.src = url + (url.indexOf("#") < 0 ? "#t=0.001" : ""); media.controls = true; media.playsInline = true; media.preload = "metadata"; media.loop = true;
156
201
  media.addEventListener("loadedmetadata", resize);
202
+ onlyOnePlays(media);
203
+ brokenNote(t, media, "video");
157
204
  t.appendChild(media);
205
+ withSpinner(t, media, "loadeddata");
158
206
  } else if (kind === "audio") {
159
207
  media = el("audio"); media.src = url; media.controls = true; media.preload = "metadata";
208
+ onlyOnePlays(media);
209
+ brokenNote(t, media, "audio");
160
210
  t.appendChild(media);
211
+ } else {
212
+ // 3D models, subtitles, documents and other links cannot be drawn here: show the link.
213
+ t.appendChild(el("div", "text", url.split("?")[0].split("/").pop() || url));
161
214
  }
162
- var meta = el("div", "meta"), name = url.split("?")[0].split("/").pop() || "file";
163
- meta.appendChild(el("span", "", kind === "file" ? name : kind));
164
- meta.appendChild(openButton(url, kind === "file" ? "Open" : "Open / save"));
215
+ var meta = el("div", "meta");
216
+ meta.appendChild(el("span", "", KIND_LABEL[kind] || kind));
217
+ meta.appendChild(openButton(url, kind === "file" || kind === "model" || kind === "subtitle" ? "Open" : "Open / save"));
165
218
  t.appendChild(meta);
166
219
  return t;
167
220
  }
@@ -191,10 +244,10 @@
191
244
  body.appendChild(el("div", "err", String(d.error)));
192
245
  setState("failed");
193
246
  } else {
194
- var urls = (d.outputs || []).map(urlOf).filter(Boolean);
195
- if (urls.length) {
196
- var grid = el("div", "grid" + (urls.length === 1 ? " one" : ""));
197
- urls.forEach(function (u) { var t = tile(u); if (t) grid.appendChild(t); });
247
+ var outs = (d.outputs || []).filter(function (o) { return o !== null && o !== undefined && o !== ""; });
248
+ if (outs.length) {
249
+ var grid = el("div", "grid" + (outs.length === 1 ? " one" : ""));
250
+ outs.forEach(function (o) { grid.appendChild(tile(o)); });
198
251
  body.appendChild(grid);
199
252
  setState(d.status === "completed" || d.done ? "done" : (d.status || ""));
200
253
  } else if (d.status && !d.done) {
@@ -202,7 +255,9 @@
202
255
  var label = el("span", "", "Generating…");
203
256
  w.appendChild(label); body.appendChild(w);
204
257
  started = started || Date.now();
205
- var tick = function () { var s = Math.round((Date.now() - started) / 1000); label.textContent = "Generating… " + s + " s. Video can take a few minutes."; };
258
+ // Only a video run says it can take minutes; images and audio finish much sooner.
259
+ var isVideo = /video|t2v|i2v/i.test(String(d.model || ""));
260
+ var tick = function () { var s = Math.round((Date.now() - started) / 1000); label.textContent = "Generating… " + s + " s." + (isVideo ? " Video can take a few minutes." : ""); };
206
261
  tick(); ticker = setInterval(tick, 1000);
207
262
  setState(d.status);
208
263
  } else if (d.note) {
@@ -223,6 +278,28 @@
223
278
  resize();
224
279
  }
225
280
 
281
+ // The card can no longer follow the run (the host refused its tool call, or it gave up): stop the timer and
282
+ // the spinner so it does not look as if it is still generating, and say where the result will appear.
283
+ function stopFollowing(message) {
284
+ clearInterval(ticker);
285
+ var body = $("body");
286
+ if (!(current && (current.outputs || []).length)) {
287
+ body.textContent = "";
288
+ body.appendChild(el("p", "", "Started. This card stops here; the result appears when the assistant checks on the run."));
289
+ setState("submitted");
290
+ }
291
+ $("note").textContent = message; $("note").hidden = false; resize();
292
+ }
293
+
294
+ // Say what actually went wrong when the host refused the card's tool call, so it can be told apart from a timeout.
295
+ function whyNot(err) {
296
+ if (window.console) console.warn("wavespeed-result: get_prediction failed", err, "hostCapabilities:", hostCaps);
297
+ if (err && err.message === "timeout") return "no answer from this chat in 130 seconds";
298
+ var said = "this chat said: " + ((err && err.message) || "error") + (err && err.code !== undefined ? " [" + err.code + "]" : "");
299
+ if (hostCaps && !hostCaps.serverTools) said += "; it does not declare tool calls for cards";
300
+ return said;
301
+ }
302
+
226
303
  // Keep asking the server while the run is going: this is what turns a video's wait into a card that fills in.
227
304
  function poll(id, token) {
228
305
  var asked = 0;
@@ -237,16 +314,15 @@
237
314
  try { d = JSON.parse(text); } catch (e) {}
238
315
  }
239
316
  if (res && res.isError && !(d && d.error)) d = { error: ((res.content || [])[0] || {}).text || "The run failed." };
240
- if (!d) { $("note").textContent = "Still running. Ask the assistant to check on it."; $("note").hidden = false; return; }
317
+ if (!d) { stopFollowing("Still running. Ask the assistant to check on it."); return; }
241
318
  if (d.error || d.done || ["completed", "failed", "cancelled", "timeout", "deleted"].indexOf(d.status) >= 0) {
242
319
  if (d.status && d.status !== "completed" && !d.error) d.error = "Prediction " + d.status + (d.error ? ": " + d.error : "");
243
320
  current = d; render(d); return;
244
321
  }
245
- if (asked < 80) again(); else { $("note").textContent = "Still running on the server. Ask the assistant to check on it."; $("note").hidden = false; }
246
- }).catch(function () {
322
+ if (asked < 80) again(); else stopFollowing("Still running on the server. Ask the assistant to check on it.");
323
+ }).catch(function (err) {
247
324
  if (token !== pollToken) return;
248
- $("note").textContent = "This chat cannot refresh the card by itself. Ask the assistant to check on the run.";
249
- $("note").hidden = false; resize();
325
+ stopFollowing("The card could not check on the run (" + whyNot(err) + "). Ask the assistant to check on it.");
250
326
  });
251
327
  })();
252
328
  }
@@ -262,7 +338,7 @@
262
338
 
263
339
  // Handshake with the host, then wait for tool-input and tool-result.
264
340
  request("ui/initialize", { protocolVersion: PROTOCOL, appInfo: { name: "wavespeed-result", version: "1.0.0" }, appCapabilities: {} })
265
- .then(function (r) { if (r && r.hostContext) applyContext(r.hostContext); notify("ui/notifications/initialized", {}); })
341
+ .then(function (r) { hostCaps = (r && r.hostCapabilities) || {}; if (r && r.hostContext) applyContext(r.hostContext); notify("ui/notifications/initialized", {}); })
266
342
  .catch(function () { notify("ui/notifications/initialized", {}); });
267
343
 
268
344
  // ChatGPT's own bridge, when the host offers it instead of the standard messages.
package/dist/server.js CHANGED
@@ -160,7 +160,16 @@ function ok(payload) {
160
160
  return res;
161
161
  }
162
162
  export function createServer() {
163
- const server = new McpServer({ name: 'wavespeed', version: PKG_VERSION }, {
163
+ const server = new McpServer({
164
+ name: 'wavespeed',
165
+ title: 'WaveSpeed',
166
+ version: PKG_VERSION,
167
+ websiteUrl: 'https://wavespeed.ai',
168
+ icons: [
169
+ { src: 'https://wavespeed.ai/favicon.svg', mimeType: 'image/svg+xml', sizes: ['any'] },
170
+ { src: 'https://wavespeed.ai/apple-touch-icon.png', mimeType: 'image/png', sizes: ['192x192'] },
171
+ ],
172
+ }, {
164
173
  instructions: [
165
174
  'WaveSpeed AI media generation (image, video, audio, 3D).',
166
175
  'Pattern: list_models to find a model, get_model_schema to see its',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wavespeed/mcp",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
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": {