@wavespeed/mcp 1.2.2 → 1.2.4
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 +53 -10
- package/dist/lib/app.js +34 -1
- package/dist/server.js +28 -12
- package/package.json +1 -1
package/assets/result.html
CHANGED
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
// A small MCP Apps view (https://modelcontextprotocol.io/seps/1865-mcp-apps-interactive-user-interfaces-for-mcp):
|
|
57
57
|
// it speaks JSON-RPC to the host over postMessage and shows what the tool returned.
|
|
58
58
|
var PROTOCOL = "2026-01-26";
|
|
59
|
-
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;
|
|
60
60
|
var $ = function (id) { return document.getElementById(id); };
|
|
61
61
|
|
|
62
62
|
function send(msg) { try { window.parent.postMessage(msg, "*"); } catch (e) {} }
|
|
@@ -78,7 +78,11 @@
|
|
|
78
78
|
var p = pending[m.id];
|
|
79
79
|
if (!p) return;
|
|
80
80
|
delete pending[m.id];
|
|
81
|
-
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);
|
|
82
86
|
return;
|
|
83
87
|
}
|
|
84
88
|
if (m.method === "ui/notifications/tool-input") { args = (m.params && m.params.arguments) || {}; showPrompt(); }
|
|
@@ -251,7 +255,9 @@
|
|
|
251
255
|
var label = el("span", "", "Generating…");
|
|
252
256
|
w.appendChild(label); body.appendChild(w);
|
|
253
257
|
started = started || Date.now();
|
|
254
|
-
|
|
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." : ""); };
|
|
255
261
|
tick(); ticker = setInterval(tick, 1000);
|
|
256
262
|
setState(d.status);
|
|
257
263
|
} else if (d.note) {
|
|
@@ -272,13 +278,51 @@
|
|
|
272
278
|
resize();
|
|
273
279
|
}
|
|
274
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
|
+
|
|
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
|
+
|
|
275
319
|
// Keep asking the server while the run is going: this is what turns a video's wait into a card that fills in.
|
|
276
320
|
function poll(id, token) {
|
|
277
321
|
var asked = 0;
|
|
278
322
|
(function again() {
|
|
279
323
|
if (token !== pollToken) return;
|
|
280
324
|
asked++;
|
|
281
|
-
|
|
325
|
+
callServerTool("get_prediction", { id: id, wait_seconds: 25 }).then(function (res) {
|
|
282
326
|
if (token !== pollToken) return;
|
|
283
327
|
var d = res && res.structuredContent;
|
|
284
328
|
if (!d) {
|
|
@@ -286,16 +330,15 @@
|
|
|
286
330
|
try { d = JSON.parse(text); } catch (e) {}
|
|
287
331
|
}
|
|
288
332
|
if (res && res.isError && !(d && d.error)) d = { error: ((res.content || [])[0] || {}).text || "The run failed." };
|
|
289
|
-
if (!d) {
|
|
333
|
+
if (!d) { stopFollowing("Still running. Ask the assistant to check on it."); return; }
|
|
290
334
|
if (d.error || d.done || ["completed", "failed", "cancelled", "timeout", "deleted"].indexOf(d.status) >= 0) {
|
|
291
335
|
if (d.status && d.status !== "completed" && !d.error) d.error = "Prediction " + d.status + (d.error ? ": " + d.error : "");
|
|
292
336
|
current = d; render(d); return;
|
|
293
337
|
}
|
|
294
|
-
if (asked < 80) again(); else
|
|
295
|
-
}).catch(function () {
|
|
338
|
+
if (asked < 80) again(); else stopFollowing("Still running on the server. Ask the assistant to check on it.");
|
|
339
|
+
}).catch(function (err) {
|
|
296
340
|
if (token !== pollToken) return;
|
|
297
|
-
|
|
298
|
-
$("note").hidden = false; resize();
|
|
341
|
+
stopFollowing("The card could not check on the run (" + whyNot(err) + "). Ask the assistant to check on it.");
|
|
299
342
|
});
|
|
300
343
|
})();
|
|
301
344
|
}
|
|
@@ -311,7 +354,7 @@
|
|
|
311
354
|
|
|
312
355
|
// Handshake with the host, then wait for tool-input and tool-result.
|
|
313
356
|
request("ui/initialize", { protocolVersion: PROTOCOL, appInfo: { name: "wavespeed-result", version: "1.0.0" }, appCapabilities: {} })
|
|
314
|
-
.then(function (r) { if (r && r.hostContext) applyContext(r.hostContext); notify("ui/notifications/initialized", {}); })
|
|
357
|
+
.then(function (r) { hostCaps = (r && r.hostCapabilities) || {}; if (r && r.hostContext) applyContext(r.hostContext); notify("ui/notifications/initialized", {}); })
|
|
315
358
|
.catch(function () { notify("ui/notifications/initialized", {}); });
|
|
316
359
|
|
|
317
360
|
// ChatGPT's own bridge, when the host offers it instead of the standard messages.
|
package/dist/lib/app.js
CHANGED
|
@@ -6,9 +6,10 @@ import { readFileSync } from 'node:fs';
|
|
|
6
6
|
export const RESULT_VIEW_URI = 'ui://wavespeed/result.html';
|
|
7
7
|
export const APP_MIME_TYPE = 'text/html;profile=mcp-app';
|
|
8
8
|
export const TOP_UP_URL = 'https://wavespeed.ai/top-up';
|
|
9
|
+
// get_prediction is not one of them: the card asks it for itself while the run goes on, and an assistant that polls it
|
|
10
|
+
// would draw a second card for the same result. It is marked callable from the card instead (see toolMeta).
|
|
9
11
|
export const UI_TOOLS = new Set([
|
|
10
12
|
'run_model',
|
|
11
|
-
'get_prediction',
|
|
12
13
|
'generate_image',
|
|
13
14
|
'edit_image',
|
|
14
15
|
'generate_video',
|
|
@@ -18,6 +19,38 @@ export const UI_TOOLS = new Set([
|
|
|
18
19
|
// "https:" alone should allow every https source, but a host may only honour named hosts when it builds the
|
|
19
20
|
// card's CSP, so the usual media hosts are named too, as wildcards.
|
|
20
21
|
export const VIEW_RESOURCE_DOMAINS = ['https:', 'https://*.cloudfront.net', 'https://*.wavespeed.ai', 'https://*.amazonaws.com'];
|
|
22
|
+
// The one-line status ChatGPT shows while a tool runs and after it ran (it allows 64 characters).
|
|
23
|
+
const TOOL_STATUS = {
|
|
24
|
+
run_model: ['Starting the run…', 'Run started'],
|
|
25
|
+
generate_image: ['Starting the run…', 'Run started'],
|
|
26
|
+
edit_image: ['Starting the run…', 'Run started'],
|
|
27
|
+
generate_video: ['Starting the run…', 'Run started'],
|
|
28
|
+
edit_video: ['Starting the run…', 'Run started'],
|
|
29
|
+
extend_video: ['Starting the run…', 'Run started'],
|
|
30
|
+
get_prediction: ['Checking the run…', 'Run checked'],
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The tool's _meta for the card: the run tools open it; get_prediction does not but says the card may call it
|
|
34
|
+
* (ChatGPT only lets a widget call a tool marked openai/widgetAccessible; MCP Apps hosts read ui.visibility, whose
|
|
35
|
+
* default already includes "app"). Same as the remote server (api-server mcp_app.go).
|
|
36
|
+
*/
|
|
37
|
+
export function toolMeta(name) {
|
|
38
|
+
const linked = UI_TOOLS.has(name);
|
|
39
|
+
if (!linked && name !== 'get_prediction')
|
|
40
|
+
return undefined;
|
|
41
|
+
const meta = {
|
|
42
|
+
ui: linked ? { resourceUri: RESULT_VIEW_URI, visibility: ['model', 'app'] } : { visibility: ['model', 'app'] },
|
|
43
|
+
'openai/widgetAccessible': true,
|
|
44
|
+
};
|
|
45
|
+
if (linked)
|
|
46
|
+
meta['openai/outputTemplate'] = RESULT_VIEW_URI;
|
|
47
|
+
const status = TOOL_STATUS[name];
|
|
48
|
+
if (status) {
|
|
49
|
+
meta['openai/toolInvocation/invoking'] = status[0];
|
|
50
|
+
meta['openai/toolInvocation/invoked'] = status[1];
|
|
51
|
+
}
|
|
52
|
+
return meta;
|
|
53
|
+
}
|
|
21
54
|
export function loadView() {
|
|
22
55
|
return readFileSync(new URL('../../assets/result.html', import.meta.url), 'utf8');
|
|
23
56
|
}
|
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,
|
|
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';
|
|
@@ -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',
|
|
@@ -194,8 +203,9 @@ export function createServer() {
|
|
|
194
203
|
}
|
|
195
204
|
};
|
|
196
205
|
const tool = registerTool(...args);
|
|
197
|
-
|
|
198
|
-
|
|
206
|
+
const meta = toolMeta(name);
|
|
207
|
+
if (meta)
|
|
208
|
+
tool.update({ _meta: meta });
|
|
199
209
|
return tool;
|
|
200
210
|
};
|
|
201
211
|
server.registerResource('result-card', RESULT_VIEW_URI, {
|
|
@@ -208,7 +218,13 @@ export function createServer() {
|
|
|
208
218
|
uri: uri.href,
|
|
209
219
|
mimeType: APP_MIME_TYPE,
|
|
210
220
|
text: loadView(),
|
|
211
|
-
_meta: {
|
|
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
|
+
},
|
|
212
228
|
},
|
|
213
229
|
],
|
|
214
230
|
}));
|
|
@@ -220,7 +236,7 @@ export function createServer() {
|
|
|
220
236
|
.describe('Filter by modality type, e.g. text-to-image, image-to-video'),
|
|
221
237
|
limit: z.number().int().min(1).max(200).default(30).describe('Max results'),
|
|
222
238
|
refresh: z.boolean().default(false).describe('Bypass the 1h catalog cache'),
|
|
223
|
-
}, { readOnlyHint: true, openWorldHint: true }, async ({ query, type, limit, refresh }) => {
|
|
239
|
+
}, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }, async ({ query, type, limit, refresh }) => {
|
|
224
240
|
const models = await fetchModels({ refresh });
|
|
225
241
|
const q = query?.toLowerCase();
|
|
226
242
|
const filtered = models.filter((m) => {
|
|
@@ -237,7 +253,7 @@ export function createServer() {
|
|
|
237
253
|
models: filtered.slice(0, limit).map(compactModel),
|
|
238
254
|
});
|
|
239
255
|
});
|
|
240
|
-
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 }) => {
|
|
241
257
|
const models = await fetchModels();
|
|
242
258
|
const meta = models.find((m) => m.model_id === model);
|
|
243
259
|
if (!meta) {
|
|
@@ -427,7 +443,7 @@ export function createServer() {
|
|
|
427
443
|
const data = await fetchPricing(model, priceInput);
|
|
428
444
|
return ok({ ...data, estimate: true, disclaimer: PRICE_DISCLAIMER, playground_url: modelPlaygroundUrl(model) });
|
|
429
445
|
});
|
|
430
|
-
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 () => {
|
|
431
447
|
return ok({ ...(await fetchBalance()), top_up_url: TOP_UP_URL });
|
|
432
448
|
});
|
|
433
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.', {
|
|
@@ -437,7 +453,7 @@ export function createServer() {
|
|
|
437
453
|
status: z.enum(PREDICTION_STATUSES).optional(),
|
|
438
454
|
since: z.string().optional().describe('Created after this time (ISO or YYYY-MM-DD)'),
|
|
439
455
|
until: z.string().optional().describe('Created before this time (ISO or YYYY-MM-DD)'),
|
|
440
|
-
}, { 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 }) => {
|
|
441
457
|
return ok(await fetchHistory({
|
|
442
458
|
page,
|
|
443
459
|
pageSize: limit,
|
|
@@ -455,7 +471,7 @@ export function createServer() {
|
|
|
455
471
|
key: z.string().optional().describe('Filter by the API key uuid that generated the charge'),
|
|
456
472
|
since: z.string().optional().describe('Records after this time (ISO or YYYY-MM-DD)'),
|
|
457
473
|
until: z.string().optional().describe('Records before this time (ISO or YYYY-MM-DD)'),
|
|
458
|
-
}, { 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 }) => {
|
|
459
475
|
return ok(await fetchBillings({
|
|
460
476
|
page,
|
|
461
477
|
pageSize: limit,
|
|
@@ -470,7 +486,7 @@ export function createServer() {
|
|
|
470
486
|
since: z.string().optional().describe('Start (ISO or YYYY-MM-DD); default 7 days ago'),
|
|
471
487
|
until: z.string().optional().describe('End (ISO or YYYY-MM-DD); default now'),
|
|
472
488
|
model: z.string().optional().describe('Filter by model uuid'),
|
|
473
|
-
}, { readOnlyHint: true, openWorldHint: true }, async ({ since, until, model }) => {
|
|
489
|
+
}, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }, async ({ since, until, model }) => {
|
|
474
490
|
const end = toIso(until, 'until') ?? new Date().toISOString();
|
|
475
491
|
const start = toIso(since, 'since') ?? new Date(Date.parse(end) - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
476
492
|
return ok(await fetchUsageStats(start, end, model));
|
|
@@ -495,7 +511,7 @@ export function createServer() {
|
|
|
495
511
|
.max(1800)
|
|
496
512
|
.default(0)
|
|
497
513
|
.describe('Max seconds to wait for a terminal status; 0 = return the current status now'),
|
|
498
|
-
}, { readOnlyHint: true, openWorldHint: true }, async ({ id, wait_seconds }, extra) => {
|
|
514
|
+
}, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }, async ({ id, wait_seconds }, extra) => {
|
|
499
515
|
const started = Date.now();
|
|
500
516
|
const { item, done } = await waitForPrediction(id, {
|
|
501
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
|
+
"version": "1.2.4",
|
|
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": {
|