@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.
- package/assets/result.html +110 -34
- package/dist/server.js +10 -1
- package/package.json +1 -1
package/assets/result.html
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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 (
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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")
|
|
163
|
-
meta.appendChild(el("span", "", 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
|
|
195
|
-
if (
|
|
196
|
-
var grid = el("div", "grid" + (
|
|
197
|
-
|
|
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
|
-
|
|
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) {
|
|
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
|
|
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
|
-
|
|
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({
|
|
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.
|
|
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": {
|