@wavespeed/mcp 1.0.1 → 1.0.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/README.md +9 -0
- package/README.zh.md +9 -0
- package/dist/server.js +22 -11
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="https://wavespeed.ai" target="_blank">
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/wavespeed-logo-dark.svg">
|
|
5
|
+
<img src="assets/wavespeed-logo-light.svg" alt="WaveSpeed" width="342" height="48"/>
|
|
6
|
+
</picture>
|
|
7
|
+
</a>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
1
10
|
# WaveSpeed MCP Server
|
|
2
11
|
|
|
3
12
|
[中文](README.zh.md)
|
package/README.zh.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="https://wavespeed.ai" target="_blank">
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/wavespeed-logo-dark.svg">
|
|
5
|
+
<img src="assets/wavespeed-logo-light.svg" alt="WaveSpeed" width="342" height="48"/>
|
|
6
|
+
</picture>
|
|
7
|
+
</a>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
1
10
|
# WaveSpeed MCP Server
|
|
2
11
|
|
|
3
12
|
[English](README.md)
|
package/dist/server.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// hosted URL. Bare paths pass through untouched.
|
|
12
12
|
// * Price quotes name the inputs they were blind to instead of presenting
|
|
13
13
|
// the formula's floor as "the" price.
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
14
15
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
15
16
|
import { z } from 'zod';
|
|
16
17
|
import { Client } from 'wavespeed';
|
|
@@ -19,6 +20,8 @@ import { getApiKey, getBaseUrl } from './lib/config.js';
|
|
|
19
20
|
import { resolveLocalFiles } from './lib/local-files.js';
|
|
20
21
|
import { uploadWithCache } from './lib/upload-cache.js';
|
|
21
22
|
import { missingPriceVars, isFloorQuote } from './lib/pricing-vars.js';
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
const { version: PKG_VERSION } = require('../package.json');
|
|
22
25
|
const PRICE_DISCLAIMER = 'Estimate only, for reference — the amount actually charged for a run is authoritative.';
|
|
23
26
|
function requestSchema(m) {
|
|
24
27
|
return m.api_schema?.api_schemas?.[0]?.request_schema;
|
|
@@ -43,7 +46,7 @@ function ok(payload) {
|
|
|
43
46
|
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
44
47
|
}
|
|
45
48
|
export function createServer() {
|
|
46
|
-
const server = new McpServer({ name: 'wavespeed', version:
|
|
49
|
+
const server = new McpServer({ name: 'wavespeed', version: PKG_VERSION }, {
|
|
47
50
|
instructions: [
|
|
48
51
|
'WaveSpeed AI media generation (image, video, audio, 3D).',
|
|
49
52
|
'Pattern: list_models to find a model, get_model_schema to see its',
|
|
@@ -61,7 +64,7 @@ export function createServer() {
|
|
|
61
64
|
.describe('Filter by modality type, e.g. text-to-image, image-to-video'),
|
|
62
65
|
limit: z.number().int().min(1).max(200).default(30).describe('Max results'),
|
|
63
66
|
refresh: z.boolean().default(false).describe('Bypass the 1h catalog cache'),
|
|
64
|
-
}, async ({ query, type, limit, refresh }) => {
|
|
67
|
+
}, { readOnlyHint: true, openWorldHint: true }, async ({ query, type, limit, refresh }) => {
|
|
65
68
|
const models = await fetchModels({ refresh });
|
|
66
69
|
const q = query?.toLowerCase();
|
|
67
70
|
const filtered = models.filter((m) => {
|
|
@@ -78,7 +81,7 @@ export function createServer() {
|
|
|
78
81
|
models: filtered.slice(0, limit).map(compactModel),
|
|
79
82
|
});
|
|
80
83
|
});
|
|
81
|
-
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. google/nano-banana-2/text-to-image') }, async ({ model }) => {
|
|
84
|
+
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. google/nano-banana-2/text-to-image') }, { readOnlyHint: true, openWorldHint: true }, async ({ model }) => {
|
|
82
85
|
const models = await fetchModels();
|
|
83
86
|
const meta = models.find((m) => m.model_id === model);
|
|
84
87
|
if (!meta) {
|
|
@@ -106,7 +109,7 @@ export function createServer() {
|
|
|
106
109
|
.max(1800)
|
|
107
110
|
.default(600)
|
|
108
111
|
.describe('Max seconds to wait; 0 = submit only and return the prediction id'),
|
|
109
|
-
}, async ({ model, input, wait_seconds }) => {
|
|
112
|
+
}, { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ model, input, wait_seconds }) => {
|
|
110
113
|
const resolved = await resolveLocalFiles(input, {
|
|
111
114
|
upload: async (p) => (await uploadFile(p)).url,
|
|
112
115
|
});
|
|
@@ -131,8 +134,16 @@ export function createServer() {
|
|
|
131
134
|
server.tool('get_price', 'Estimate the cost of a run before executing it (no charge). Provide the same input you would pass to run_model — pricing often depends on inputs like duration or resolution.', {
|
|
132
135
|
model: z.string().describe('Model ID from list_models'),
|
|
133
136
|
input: z.record(z.unknown()).default({}).describe('Inputs the quote should account for'),
|
|
134
|
-
}, async ({ model, input }) => {
|
|
135
|
-
|
|
137
|
+
}, { readOnlyHint: false, destructiveHint: false, openWorldHint: true }, async ({ model, input }) => {
|
|
138
|
+
// Resolve @path markers exactly like run_model: pricing formulas often
|
|
139
|
+
// read the file itself (get_duration_v3(audio)), so quoting with the
|
|
140
|
+
// literal "@./a.mp3" string would silently collapse to the floor. The
|
|
141
|
+
// 24h content-hash cache means the later run reuses this upload.
|
|
142
|
+
const resolved = await resolveLocalFiles(input, {
|
|
143
|
+
upload: async (p) => (await uploadFile(p)).url,
|
|
144
|
+
});
|
|
145
|
+
const priceInput = resolved.input;
|
|
146
|
+
const data = await fetchPricing(model, priceInput);
|
|
136
147
|
// The pricing endpoint accepts partial inputs without complaint — a
|
|
137
148
|
// formula whose variables are all missing collapses to base_price, the
|
|
138
149
|
// floor of the model's range. Name what the quote could not see.
|
|
@@ -141,8 +152,8 @@ export function createServer() {
|
|
|
141
152
|
try {
|
|
142
153
|
const models = await fetchModels();
|
|
143
154
|
const meta = models.find((m) => m.model_id === model);
|
|
144
|
-
unpriced = missingPriceVars(meta?.formula,
|
|
145
|
-
atFloor = isFloorQuote(meta?.formula,
|
|
155
|
+
unpriced = missingPriceVars(meta?.formula, priceInput, requestSchema(meta ?? {})?.properties);
|
|
156
|
+
atFloor = isFloorQuote(meta?.formula, priceInput);
|
|
146
157
|
}
|
|
147
158
|
catch {
|
|
148
159
|
/* no catalog — the generic disclaimer still applies */
|
|
@@ -155,13 +166,13 @@ export function createServer() {
|
|
|
155
166
|
disclaimer: PRICE_DISCLAIMER,
|
|
156
167
|
});
|
|
157
168
|
});
|
|
158
|
-
server.tool('get_balance', 'Show the WaveSpeed account credit balance.', {}, async () => {
|
|
169
|
+
server.tool('get_balance', 'Show the WaveSpeed account credit balance.', {}, { readOnlyHint: true, openWorldHint: true }, async () => {
|
|
159
170
|
return ok(await fetchBalance());
|
|
160
171
|
});
|
|
161
|
-
server.tool('upload_file', 'Upload a local file to WaveSpeed and get its hosted URL (identical bytes reuse the same upload for 24h). Usually unnecessary — run_model handles "@./path" inputs itself.', { path: z.string().describe('Local file path') }, async ({ path: filePath }) => {
|
|
172
|
+
server.tool('upload_file', 'Upload a local file to WaveSpeed and get its hosted URL (identical bytes reuse the same upload for 24h). Usually unnecessary — run_model handles "@./path" inputs itself.', { path: z.string().describe('Local file path') }, { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ path: filePath }) => {
|
|
162
173
|
return ok(await uploadFile(filePath));
|
|
163
174
|
});
|
|
164
|
-
server.tool('get_prediction', 'Fetch the status and outputs of a past or in-flight prediction by id — use to recover a run that hit the wait limit.', { id: z.string().describe('Prediction id returned by run_model') }, async ({ id }) => {
|
|
175
|
+
server.tool('get_prediction', 'Fetch the status and outputs of a past or in-flight prediction by id — use to recover a run that hit the wait limit.', { id: z.string().describe('Prediction id returned by run_model') }, { readOnlyHint: true, openWorldHint: true }, async ({ id }) => {
|
|
165
176
|
const item = await fetchPrediction(id);
|
|
166
177
|
return ok({
|
|
167
178
|
id: item.id,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wavespeed/mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "WaveSpeed MCP server \u2014 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": {
|
|
@@ -47,5 +47,6 @@
|
|
|
47
47
|
"tsx": "^4.19.0",
|
|
48
48
|
"typescript": "^5.7.0",
|
|
49
49
|
"vitest": "^3.0.0"
|
|
50
|
-
}
|
|
50
|
+
},
|
|
51
|
+
"mcpName": "ai.wavespeed/mcp"
|
|
51
52
|
}
|