@wavespeed/mcp 1.1.3 → 1.2.1
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 +279 -0
- package/dist/lib/app.js +46 -0
- package/dist/server.js +44 -1
- package/package.json +1 -1
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>WaveSpeed result</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* Tokens follow the WaveSpeed site (api-dashboard globals.css): near-black/white ink, card surfaces one step off the
|
|
9
|
+
page, 0.5rem radius, and buttons that are black on light and white on dark. */
|
|
10
|
+
:root { color-scheme: light dark; --fg: #0a0a0a; --muted: #3c4a5c; --line: #dde2ea; --card: #f5f5f7; --primary: #3f74ff; --bad: #dc2626; --hover: #eef2f7; --bg: #fff; }
|
|
11
|
+
:root[data-theme="dark"] { --fg: #fff; --muted: #94a3b8; --line: #404040; --card: #262626; --hover: #383838; --bad: #f05252; --bg: #171717; }
|
|
12
|
+
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --fg: #fff; --muted: #94a3b8; --line: #404040; --card: #262626; --hover: #383838; --bad: #f05252; --bg: #171717; } }
|
|
13
|
+
* { box-sizing: border-box; }
|
|
14
|
+
body { margin: 0; padding: 12px; font: 14px/1.5 var(--font-sans, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif); color: var(--fg); background: transparent; -webkit-font-smoothing: antialiased; }
|
|
15
|
+
.head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
|
|
16
|
+
.model { font-weight: 600; word-break: break-all; }
|
|
17
|
+
.state { color: var(--muted); font-size: 12px; white-space: nowrap; }
|
|
18
|
+
.prompt { color: var(--muted); margin: 0 0 10px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
19
|
+
.grid { display: grid; gap: 8px; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); }
|
|
20
|
+
.grid.one { grid-template-columns: 1fr; }
|
|
21
|
+
.tile { background: var(--card); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
|
|
22
|
+
.tile img, .tile video { display: block; width: 100%; max-height: 520px; object-fit: contain; background: var(--card); }
|
|
23
|
+
.tile video { background: #000; }
|
|
24
|
+
.broken { padding: 28px 12px; text-align: center; color: var(--muted); font-size: 13px; }
|
|
25
|
+
.tile img { cursor: zoom-in; }
|
|
26
|
+
.tile audio { display: block; width: 100%; padding: 10px; }
|
|
27
|
+
.tile .meta { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 6px 8px 6px 12px; font-size: 12px; color: var(--muted); }
|
|
28
|
+
/* Outline button, like the site's Button variant="outline" (small). */
|
|
29
|
+
button, a.btn { font: inherit; font-size: 12px; font-weight: 500; height: 32px; padding: 0 12px; color: var(--fg); background: var(--bg); border: 1px solid var(--line); border-radius: 6px; cursor: pointer; text-decoration: none; transition: background-color .15s; }
|
|
30
|
+
button:hover, a.btn:hover { background: var(--hover); }
|
|
31
|
+
button:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
|
|
32
|
+
/* Primary action, like the site's default Button: black on light, white on dark. */
|
|
33
|
+
button.primary { height: 36px; padding: 0 16px; font-size: 14px; color: var(--bg); background: var(--fg); border-color: var(--fg); }
|
|
34
|
+
button.primary:hover { background: var(--fg); opacity: .9; }
|
|
35
|
+
.wait { display: flex; align-items: center; gap: 10px; padding: 16px; background: var(--card); border: 1px solid var(--line); border-radius: 8px; color: var(--muted); }
|
|
36
|
+
.spin { width: 18px; height: 18px; border: 2px solid var(--line); border-top-color: var(--primary); border-radius: 50%; animation: s 0.9s linear infinite; flex: none; }
|
|
37
|
+
@keyframes s { to { transform: rotate(360deg); } }
|
|
38
|
+
.err { padding: 12px; border: 1px solid var(--bad); border-radius: 8px; color: var(--bad); }
|
|
39
|
+
.actions { margin-top: 10px; display: flex; gap: 8px; flex-wrap: wrap; }
|
|
40
|
+
.note { margin-top: 8px; color: var(--muted); font-size: 12px; }
|
|
41
|
+
[hidden] { display: none !important; }
|
|
42
|
+
</style>
|
|
43
|
+
</head>
|
|
44
|
+
<body>
|
|
45
|
+
<div class="head"><span class="model" id="model">WaveSpeed</span><span class="state" id="state"></span></div>
|
|
46
|
+
<p class="prompt" id="prompt" hidden></p>
|
|
47
|
+
<div id="body"></div>
|
|
48
|
+
<div class="actions" id="actions" hidden></div>
|
|
49
|
+
<div class="note" id="note" hidden></div>
|
|
50
|
+
<script>
|
|
51
|
+
(function () {
|
|
52
|
+
"use strict";
|
|
53
|
+
// A small MCP Apps view (https://modelcontextprotocol.io/seps/1865-mcp-apps-interactive-user-interfaces-for-mcp):
|
|
54
|
+
// it speaks JSON-RPC to the host over postMessage and shows what the tool returned.
|
|
55
|
+
var PROTOCOL = "2026-01-26";
|
|
56
|
+
var nextId = 1, pending = {}, args = {}, current = null, pollToken = 0, started = 0, ticker = null;
|
|
57
|
+
var $ = function (id) { return document.getElementById(id); };
|
|
58
|
+
|
|
59
|
+
function send(msg) { try { window.parent.postMessage(msg, "*"); } catch (e) {} }
|
|
60
|
+
function request(method, params) {
|
|
61
|
+
return new Promise(function (resolve, reject) {
|
|
62
|
+
var id = nextId++;
|
|
63
|
+
pending[id] = { resolve: resolve, reject: reject };
|
|
64
|
+
send({ jsonrpc: "2.0", id: id, method: method, params: params || {} });
|
|
65
|
+
setTimeout(function () { if (pending[id]) { delete pending[id]; reject(new Error("timeout")); } }, 130000);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function notify(method, params) { send({ jsonrpc: "2.0", method: method, params: params || {} }); }
|
|
69
|
+
|
|
70
|
+
window.addEventListener("message", function (e) {
|
|
71
|
+
if (e.source !== window.parent) return;
|
|
72
|
+
var m = e.data;
|
|
73
|
+
if (!m || m.jsonrpc !== "2.0") return;
|
|
74
|
+
if (m.id !== undefined && !m.method) {
|
|
75
|
+
var p = pending[m.id];
|
|
76
|
+
if (!p) return;
|
|
77
|
+
delete pending[m.id];
|
|
78
|
+
if (m.error) p.reject(new Error(m.error.message || "error")); else p.resolve(m.result);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (m.method === "ui/notifications/tool-input") { args = (m.params && m.params.arguments) || {}; showPrompt(); }
|
|
82
|
+
else if (m.method === "ui/notifications/tool-result") { show(m.params || {}); }
|
|
83
|
+
else if (m.method === "ui/notifications/host-context-changed") { applyContext(m.params || {}); }
|
|
84
|
+
else if (m.method === "ui/notifications/tool-cancelled") { pollToken++; setState("cancelled"); }
|
|
85
|
+
else if (m.method === "ui/resource-teardown" && m.id !== undefined) { pollToken++; send({ jsonrpc: "2.0", id: m.id, result: {} }); }
|
|
86
|
+
else if (m.method === "ping" && m.id !== undefined) { send({ jsonrpc: "2.0", id: m.id, result: {} }); }
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// The host's own look: its theme, its font variables (--font-sans) and the @font-face rules that load them.
|
|
90
|
+
function applyContext(ctx) {
|
|
91
|
+
if (ctx.theme === "dark" || ctx.theme === "light") document.documentElement.setAttribute("data-theme", ctx.theme);
|
|
92
|
+
var st = ctx.styles || {};
|
|
93
|
+
var vars = st.variables || {};
|
|
94
|
+
Object.keys(vars).forEach(function (k) {
|
|
95
|
+
if (/^--font-/.test(k) && typeof vars[k] === "string") document.documentElement.style.setProperty(k, vars[k]);
|
|
96
|
+
});
|
|
97
|
+
var fonts = st.css && st.css.fonts;
|
|
98
|
+
if (typeof fonts === "string" && fonts) {
|
|
99
|
+
var tag = document.getElementById("host-fonts");
|
|
100
|
+
if (!tag) { tag = document.createElement("style"); tag.id = "host-fonts"; document.head.appendChild(tag); }
|
|
101
|
+
tag.textContent = fonts;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function openLink(url) {
|
|
105
|
+
if (!/^https:\/\//i.test(url)) return;
|
|
106
|
+
request("ui/open-link", { url: url }).catch(function () {});
|
|
107
|
+
}
|
|
108
|
+
function setState(t) { $("state").textContent = t || ""; }
|
|
109
|
+
function showPrompt() {
|
|
110
|
+
var text = args.prompt || (args.input && args.input.prompt) || "";
|
|
111
|
+
var el = $("prompt");
|
|
112
|
+
if (text) { el.textContent = String(text); el.hidden = false; } else { el.hidden = true; }
|
|
113
|
+
resize();
|
|
114
|
+
}
|
|
115
|
+
|
|
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
|
+
}
|
|
121
|
+
function kindOf(url) {
|
|
122
|
+
var path = url.split("?")[0].split("#")[0].toLowerCase();
|
|
123
|
+
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";
|
|
127
|
+
return "file";
|
|
128
|
+
}
|
|
129
|
+
function el(tag, cls, text) {
|
|
130
|
+
var n = document.createElement(tag);
|
|
131
|
+
if (cls) n.className = cls;
|
|
132
|
+
if (text !== undefined) n.textContent = text;
|
|
133
|
+
return n;
|
|
134
|
+
}
|
|
135
|
+
function openButton(url, label) {
|
|
136
|
+
var b = el("button", "", label || "Open");
|
|
137
|
+
b.addEventListener("click", function () { openLink(url); });
|
|
138
|
+
return b;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function tile(url) {
|
|
142
|
+
var kind = kindOf(url), t = el("div", "tile"), media;
|
|
143
|
+
if (!/^https?:\/\//i.test(url)) return null;
|
|
144
|
+
if (kind === "image") {
|
|
145
|
+
media = el("img"); media.src = url; media.alt = "Generated image"; media.loading = "lazy";
|
|
146
|
+
media.addEventListener("click", function () { openLink(url); });
|
|
147
|
+
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
|
+
});
|
|
153
|
+
t.appendChild(media);
|
|
154
|
+
} else if (kind === "video") {
|
|
155
|
+
media = el("video"); media.src = url; media.controls = true; media.playsInline = true; media.preload = "metadata";
|
|
156
|
+
media.addEventListener("loadedmetadata", resize);
|
|
157
|
+
t.appendChild(media);
|
|
158
|
+
} else if (kind === "audio") {
|
|
159
|
+
media = el("audio"); media.src = url; media.controls = true; media.preload = "metadata";
|
|
160
|
+
t.appendChild(media);
|
|
161
|
+
}
|
|
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"));
|
|
165
|
+
t.appendChild(meta);
|
|
166
|
+
return t;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function show(result) {
|
|
170
|
+
var data = result.structuredContent;
|
|
171
|
+
if (!data) {
|
|
172
|
+
// No structured data (an error, or a host that drops it): say what the tool said.
|
|
173
|
+
var text = (result.content || []).map(function (c) { return c && c.text || ""; }).join("\n").trim();
|
|
174
|
+
try { var parsed = JSON.parse(text); if (parsed && typeof parsed === "object") data = parsed; } catch (e) {}
|
|
175
|
+
if (!data) data = result.isError ? { error: text || "The request failed." } : { note: text };
|
|
176
|
+
}
|
|
177
|
+
current = data;
|
|
178
|
+
pollToken++;
|
|
179
|
+
render(data);
|
|
180
|
+
var running = data.status && !data.done && !data.error && ["created", "processing", "queued", "pending"].indexOf(data.status) >= 0;
|
|
181
|
+
if (running && data.id) poll(data.id, pollToken);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function render(d) {
|
|
185
|
+
var body = $("body"), actions = $("actions"), note = $("note");
|
|
186
|
+
body.textContent = ""; actions.textContent = ""; actions.hidden = true; note.hidden = true;
|
|
187
|
+
if (d.model) $("model").textContent = d.model;
|
|
188
|
+
clearInterval(ticker);
|
|
189
|
+
|
|
190
|
+
if (d.error) {
|
|
191
|
+
body.appendChild(el("div", "err", String(d.error)));
|
|
192
|
+
setState("failed");
|
|
193
|
+
} 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); });
|
|
198
|
+
body.appendChild(grid);
|
|
199
|
+
setState(d.status === "completed" || d.done ? "done" : (d.status || ""));
|
|
200
|
+
} else if (d.status && !d.done) {
|
|
201
|
+
var w = el("div", "wait"); w.appendChild(el("div", "spin"));
|
|
202
|
+
var label = el("span", "", "Generating…");
|
|
203
|
+
w.appendChild(label); body.appendChild(w);
|
|
204
|
+
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."; };
|
|
206
|
+
tick(); ticker = setInterval(tick, 1000);
|
|
207
|
+
setState(d.status);
|
|
208
|
+
} else if (d.note) {
|
|
209
|
+
body.appendChild(el("p", "", String(d.note)));
|
|
210
|
+
} else if (d.status) {
|
|
211
|
+
setState(d.status);
|
|
212
|
+
}
|
|
213
|
+
if (d.price !== undefined && d.price !== null) {
|
|
214
|
+
note.textContent = "Charged about $" + (typeof d.price === "number" ? d.price.toFixed(4) : d.price);
|
|
215
|
+
note.hidden = false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (d.top_up_url) {
|
|
219
|
+
var b = el("button", "primary", "Top up credits");
|
|
220
|
+
b.addEventListener("click", function () { openLink(d.top_up_url); });
|
|
221
|
+
actions.appendChild(b); actions.hidden = false;
|
|
222
|
+
}
|
|
223
|
+
resize();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Keep asking the server while the run is going: this is what turns a video's wait into a card that fills in.
|
|
227
|
+
function poll(id, token) {
|
|
228
|
+
var asked = 0;
|
|
229
|
+
(function again() {
|
|
230
|
+
if (token !== pollToken) return;
|
|
231
|
+
asked++;
|
|
232
|
+
request("tools/call", { name: "get_prediction", arguments: { id: id, wait_seconds: 25 } }).then(function (res) {
|
|
233
|
+
if (token !== pollToken) return;
|
|
234
|
+
var d = res && res.structuredContent;
|
|
235
|
+
if (!d) {
|
|
236
|
+
var text = ((res && res.content) || []).map(function (c) { return c && c.text || ""; }).join("");
|
|
237
|
+
try { d = JSON.parse(text); } catch (e) {}
|
|
238
|
+
}
|
|
239
|
+
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; }
|
|
241
|
+
if (d.error || d.done || ["completed", "failed", "cancelled", "timeout", "deleted"].indexOf(d.status) >= 0) {
|
|
242
|
+
if (d.status && d.status !== "completed" && !d.error) d.error = "Prediction " + d.status + (d.error ? ": " + d.error : "");
|
|
243
|
+
current = d; render(d); return;
|
|
244
|
+
}
|
|
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 () {
|
|
247
|
+
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();
|
|
250
|
+
});
|
|
251
|
+
})();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
var lastHeight = 0;
|
|
255
|
+
function resize() {
|
|
256
|
+
var h = Math.ceil(document.documentElement.getBoundingClientRect().height);
|
|
257
|
+
if (h === lastHeight) return;
|
|
258
|
+
lastHeight = h;
|
|
259
|
+
notify("ui/notifications/size-changed", { width: Math.ceil(document.documentElement.getBoundingClientRect().width), height: h });
|
|
260
|
+
}
|
|
261
|
+
if (window.ResizeObserver) new ResizeObserver(resize).observe(document.body);
|
|
262
|
+
|
|
263
|
+
// Handshake with the host, then wait for tool-input and tool-result.
|
|
264
|
+
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", {}); })
|
|
266
|
+
.catch(function () { notify("ui/notifications/initialized", {}); });
|
|
267
|
+
|
|
268
|
+
// ChatGPT's own bridge, when the host offers it instead of the standard messages.
|
|
269
|
+
function fromOpenAI() {
|
|
270
|
+
var o = window.openai;
|
|
271
|
+
if (o && o.toolOutput && !current) show({ structuredContent: o.toolOutput });
|
|
272
|
+
if (o && o.theme) applyContext({ theme: o.theme });
|
|
273
|
+
}
|
|
274
|
+
window.addEventListener("openai:set_globals", fromOpenAI);
|
|
275
|
+
setTimeout(fromOpenAI, 300);
|
|
276
|
+
})();
|
|
277
|
+
</script>
|
|
278
|
+
</body>
|
|
279
|
+
</html>
|
package/dist/lib/app.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// MCP Apps (SEP-1865): the tools that start or read a run point at one HTML
|
|
2
|
+
// view, which a supporting client draws as a result card. Other clients ignore
|
|
3
|
+
// the metadata and show the text as before. The view is the same file the
|
|
4
|
+
// remote server (api-server /mcp) serves; keep the two copies identical.
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
export const RESULT_VIEW_URI = 'ui://wavespeed/result.html';
|
|
7
|
+
export const APP_MIME_TYPE = 'text/html;profile=mcp-app';
|
|
8
|
+
export const TOP_UP_URL = 'https://wavespeed.ai/top-up';
|
|
9
|
+
export const UI_TOOLS = new Set([
|
|
10
|
+
'run_model',
|
|
11
|
+
'get_prediction',
|
|
12
|
+
'generate_image',
|
|
13
|
+
'edit_image',
|
|
14
|
+
'generate_video',
|
|
15
|
+
'edit_video',
|
|
16
|
+
'extend_video',
|
|
17
|
+
]);
|
|
18
|
+
// "https:" alone should allow every https source, but a host may only honour named hosts when it builds the
|
|
19
|
+
// card's CSP, so the usual media hosts are named too, as wildcards.
|
|
20
|
+
export const VIEW_RESOURCE_DOMAINS = ['https:', 'https://*.cloudfront.net', 'https://*.wavespeed.ai', 'https://*.amazonaws.com'];
|
|
21
|
+
export function loadView() {
|
|
22
|
+
return readFileSync(new URL('../../assets/result.html', import.meta.url), 'utf8');
|
|
23
|
+
}
|
|
24
|
+
export function isInsufficientCredits(message) {
|
|
25
|
+
const m = message.toLowerCase();
|
|
26
|
+
return m.includes('insufficient credits') || m.includes('top-up is required');
|
|
27
|
+
}
|
|
28
|
+
export function topUpLink(model) {
|
|
29
|
+
const q = new URLSearchParams({ tab: 'top-up', source: 'mcp' });
|
|
30
|
+
if (typeof model === 'string' && model)
|
|
31
|
+
q.set('model', model);
|
|
32
|
+
return `${TOP_UP_URL}?${q.toString()}`;
|
|
33
|
+
}
|
|
34
|
+
export function insufficientCreditsResult(message, model) {
|
|
35
|
+
const link = topUpLink(model);
|
|
36
|
+
return {
|
|
37
|
+
isError: true,
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
type: 'text',
|
|
41
|
+
text: `${message} Tell the user their balance is too low and give them this link to add credits, then retry: ${link}`,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
structuredContent: { error: message, top_up_url: link },
|
|
45
|
+
};
|
|
46
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -24,6 +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
28
|
import { uploadWithCache } from './lib/upload-cache.js';
|
|
28
29
|
import { findUnknownInputs } from './lib/validate-inputs.js';
|
|
29
30
|
import { IMAGE_MODEL_KEYS, VIDEO_MODEL_KEYS } from './lib/media-models.js';
|
|
@@ -150,7 +151,13 @@ more = {}) {
|
|
|
150
151
|
});
|
|
151
152
|
}
|
|
152
153
|
function ok(payload) {
|
|
153
|
-
|
|
154
|
+
const res = {
|
|
155
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
156
|
+
};
|
|
157
|
+
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
|
158
|
+
res.structuredContent = payload;
|
|
159
|
+
}
|
|
160
|
+
return res;
|
|
154
161
|
}
|
|
155
162
|
export function createServer() {
|
|
156
163
|
const server = new McpServer({ name: 'wavespeed', version: PKG_VERSION }, {
|
|
@@ -169,6 +176,42 @@ export function createServer() {
|
|
|
169
176
|
'run_model returns done=false, keep the id and poll get_prediction.',
|
|
170
177
|
].join(' '),
|
|
171
178
|
});
|
|
179
|
+
// Every tool gets the same two additions: a balance that is too low comes back with the top-up link, and the
|
|
180
|
+
// tools that show a result point at the result card.
|
|
181
|
+
const registerTool = server.tool.bind(server);
|
|
182
|
+
server.tool = (...args) => {
|
|
183
|
+
const name = args[0];
|
|
184
|
+
const cb = args[args.length - 1];
|
|
185
|
+
args[args.length - 1] = async (...a) => {
|
|
186
|
+
try {
|
|
187
|
+
return await cb(...a);
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
const message = String(err?.message ?? err);
|
|
191
|
+
if (!isInsufficientCredits(message))
|
|
192
|
+
throw err;
|
|
193
|
+
return insufficientCreditsResult(message, a[0]?.model);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
const tool = registerTool(...args);
|
|
197
|
+
if (UI_TOOLS.has(name))
|
|
198
|
+
tool.update({ _meta: { ui: { resourceUri: RESULT_VIEW_URI } } });
|
|
199
|
+
return tool;
|
|
200
|
+
};
|
|
201
|
+
server.registerResource('result-card', RESULT_VIEW_URI, {
|
|
202
|
+
title: 'WaveSpeed result card',
|
|
203
|
+
description: 'Shows the images, videos and audio of a WaveSpeed run, and refreshes while it is running.',
|
|
204
|
+
mimeType: APP_MIME_TYPE,
|
|
205
|
+
}, async (uri) => ({
|
|
206
|
+
contents: [
|
|
207
|
+
{
|
|
208
|
+
uri: uri.href,
|
|
209
|
+
mimeType: APP_MIME_TYPE,
|
|
210
|
+
text: loadView(),
|
|
211
|
+
_meta: { ui: { csp: { resourceDomains: VIEW_RESOURCE_DOMAINS, connectDomains: [] }, prefersBorder: true } },
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
}));
|
|
172
215
|
server.tool('list_models', 'Search the live WaveSpeed model catalog (image, video, audio, 3D). Returns model IDs usable with run_model. Do not invent model IDs — always pick one returned by this tool.', {
|
|
173
216
|
query: z.string().optional().describe('Free-text filter on id/name/description'),
|
|
174
217
|
type: z
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wavespeed/mcp",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
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": {
|