@mindstone-engineering/mcp-server-runway 0.1.0
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/dist/auth.d.ts +21 -0
- package/dist/auth.js +29 -0
- package/dist/bridge.d.ts +16 -0
- package/dist/bridge.js +43 -0
- package/dist/client.d.ts +43 -0
- package/dist/client.js +261 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +30 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +17 -0
- package/dist/tools/account.d.ts +3 -0
- package/dist/tools/account.js +71 -0
- package/dist/tools/audio.d.ts +3 -0
- package/dist/tools/audio.js +138 -0
- package/dist/tools/configure.d.ts +3 -0
- package/dist/tools/configure.js +46 -0
- package/dist/tools/image.d.ts +3 -0
- package/dist/tools/image.js +52 -0
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.js +8 -0
- package/dist/tools/tasks.d.ts +3 -0
- package/dist/tools/tasks.js +193 -0
- package/dist/tools/video.d.ts +3 -0
- package/dist/tools/video.js +168 -0
- package/dist/tools/voices.d.ts +3 -0
- package/dist/tools/voices.js +85 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.js +68 -0
- package/dist/utils.d.ts +14 -0
- package/dist/utils.js +42 -0
- package/package.json +48 -0
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runway authentication module.
|
|
3
|
+
*
|
|
4
|
+
* API key management — stored via env var (RUNWAYML_API_SECRET)
|
|
5
|
+
* or configured at runtime via the configure_runway_api_key tool.
|
|
6
|
+
*
|
|
7
|
+
* Auth: Authorization: Bearer {key} + X-Runway-Version header on all API requests.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Get the current API key.
|
|
11
|
+
*/
|
|
12
|
+
export declare function getApiKey(): string;
|
|
13
|
+
/**
|
|
14
|
+
* Set the API key at runtime (from configure tool).
|
|
15
|
+
*/
|
|
16
|
+
export declare function setApiKey(key: string): void;
|
|
17
|
+
/**
|
|
18
|
+
* Check if an API key is configured.
|
|
19
|
+
*/
|
|
20
|
+
export declare function hasApiKey(): boolean;
|
|
21
|
+
//# sourceMappingURL=auth.d.ts.map
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runway authentication module.
|
|
3
|
+
*
|
|
4
|
+
* API key management — stored via env var (RUNWAYML_API_SECRET)
|
|
5
|
+
* or configured at runtime via the configure_runway_api_key tool.
|
|
6
|
+
*
|
|
7
|
+
* Auth: Authorization: Bearer {key} + X-Runway-Version header on all API requests.
|
|
8
|
+
*/
|
|
9
|
+
/** Runtime API key — starts from env, can be updated via configure tool. */
|
|
10
|
+
let apiKey = process.env.RUNWAYML_API_SECRET ?? '';
|
|
11
|
+
/**
|
|
12
|
+
* Get the current API key.
|
|
13
|
+
*/
|
|
14
|
+
export function getApiKey() {
|
|
15
|
+
return apiKey;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Set the API key at runtime (from configure tool).
|
|
19
|
+
*/
|
|
20
|
+
export function setApiKey(key) {
|
|
21
|
+
apiKey = key;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Check if an API key is configured.
|
|
25
|
+
*/
|
|
26
|
+
export function hasApiKey() {
|
|
27
|
+
return apiKey.trim().length > 0;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path to bridge state file, supporting both current and legacy env vars.
|
|
3
|
+
*/
|
|
4
|
+
export declare const BRIDGE_STATE_PATH: string;
|
|
5
|
+
/**
|
|
6
|
+
* Send a request to the host app bridge.
|
|
7
|
+
*
|
|
8
|
+
* The bridge is an HTTP server running inside the host app (e.g. Rebel)
|
|
9
|
+
* that handles credential management and other cross-process operations.
|
|
10
|
+
*/
|
|
11
|
+
export declare const bridgeRequest: (urlPath: string, body: Record<string, unknown>) => Promise<{
|
|
12
|
+
success: boolean;
|
|
13
|
+
warning?: string;
|
|
14
|
+
error?: string;
|
|
15
|
+
}>;
|
|
16
|
+
//# sourceMappingURL=bridge.d.ts.map
|
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import { REQUEST_TIMEOUT_MS } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Path to bridge state file, supporting both current and legacy env vars.
|
|
5
|
+
*/
|
|
6
|
+
export const BRIDGE_STATE_PATH = process.env.MCP_HOST_BRIDGE_STATE || process.env.MINDSTONE_REBEL_BRIDGE_STATE || '';
|
|
7
|
+
const loadBridgeState = () => {
|
|
8
|
+
if (!BRIDGE_STATE_PATH)
|
|
9
|
+
return null;
|
|
10
|
+
try {
|
|
11
|
+
const raw = fs.readFileSync(BRIDGE_STATE_PATH, 'utf8');
|
|
12
|
+
return JSON.parse(raw);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Send a request to the host app bridge.
|
|
20
|
+
*
|
|
21
|
+
* The bridge is an HTTP server running inside the host app (e.g. Rebel)
|
|
22
|
+
* that handles credential management and other cross-process operations.
|
|
23
|
+
*/
|
|
24
|
+
export const bridgeRequest = async (urlPath, body) => {
|
|
25
|
+
const bridge = loadBridgeState();
|
|
26
|
+
if (!bridge) {
|
|
27
|
+
return { success: false, error: 'Bridge not available' };
|
|
28
|
+
}
|
|
29
|
+
const response = await fetch(`http://127.0.0.1:${bridge.port}${urlPath}`, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
32
|
+
headers: {
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
Authorization: `Bearer ${bridge.token}`,
|
|
35
|
+
},
|
|
36
|
+
body: JSON.stringify(body),
|
|
37
|
+
});
|
|
38
|
+
if (response.status === 401 || response.status === 403) {
|
|
39
|
+
return { success: false, error: `Bridge returned ${response.status}: unauthorized. Check host app authentication.` };
|
|
40
|
+
}
|
|
41
|
+
return response.json();
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=bridge.js.map
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runway API HTTP client.
|
|
3
|
+
*
|
|
4
|
+
* Centralises Bearer auth + X-Runway-Version header injection,
|
|
5
|
+
* error handling, rate-limit messaging, and timeout handling.
|
|
6
|
+
*
|
|
7
|
+
* Auth: Authorization: Bearer {key}, X-Runway-Version: 2024-11-06
|
|
8
|
+
* Base URL: https://api.dev.runwayml.com/v1
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Make an authenticated JSON request to the Runway API.
|
|
12
|
+
*/
|
|
13
|
+
export declare function runwayFetch<T>(urlPath: string, options?: RequestInit): Promise<T>;
|
|
14
|
+
/**
|
|
15
|
+
* Make a raw authenticated fetch (for DELETE endpoints that return 204 with no body).
|
|
16
|
+
*/
|
|
17
|
+
export declare function runwayRawFetch(urlPath: string, options?: RequestInit): Promise<Response>;
|
|
18
|
+
/**
|
|
19
|
+
* Upload a local file to Runway's ephemeral storage.
|
|
20
|
+
* Returns a runway:// URI valid for 24 hours.
|
|
21
|
+
*/
|
|
22
|
+
export declare function uploadEphemeral(filePath: string): Promise<string>;
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a media input to a usable URI.
|
|
25
|
+
* - HTTPS/data/runway URIs are passed through.
|
|
26
|
+
* - Local files under the size limit are converted to data URIs.
|
|
27
|
+
* - Large local files are uploaded via ephemeral upload.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveMediaInput(input: string, category: 'image' | 'video' | 'audio'): Promise<string>;
|
|
30
|
+
/**
|
|
31
|
+
* Validate a download URL for SSRF safety.
|
|
32
|
+
* Returns an error message if the URL is unsafe, or null if OK.
|
|
33
|
+
*/
|
|
34
|
+
export declare function validateDownloadUrl(url: string): string | null;
|
|
35
|
+
export declare function costEstimate(model: string, duration: number, audio?: boolean): {
|
|
36
|
+
credits: number;
|
|
37
|
+
usd: string;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Add content moderation to a request body if specified.
|
|
41
|
+
*/
|
|
42
|
+
export declare function addContentModeration(body: Record<string, unknown>, contentMod?: string): void;
|
|
43
|
+
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runway API HTTP client.
|
|
3
|
+
*
|
|
4
|
+
* Centralises Bearer auth + X-Runway-Version header injection,
|
|
5
|
+
* error handling, rate-limit messaging, and timeout handling.
|
|
6
|
+
*
|
|
7
|
+
* Auth: Authorization: Bearer {key}, X-Runway-Version: 2024-11-06
|
|
8
|
+
* Base URL: https://api.dev.runwayml.com/v1
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { RunwayError, REQUEST_TIMEOUT_MS, RUNWAY_API_BASE, RUNWAY_API_VERSION, MIME_MAP, DATA_URI_BINARY_LIMITS, MAX_UPLOAD_BYTES, MIN_UPLOAD_BYTES, } from './types.js';
|
|
13
|
+
import { getApiKey } from './auth.js';
|
|
14
|
+
/**
|
|
15
|
+
* Make an authenticated JSON request to the Runway API.
|
|
16
|
+
*/
|
|
17
|
+
export async function runwayFetch(urlPath, options = {}) {
|
|
18
|
+
const apiKey = getApiKey();
|
|
19
|
+
if (!apiKey || apiKey.trim().length === 0) {
|
|
20
|
+
throw new RunwayError('Runway API key not configured', 'AUTH_REQUIRED', 'Configure your Runway API key in Settings. Get one at https://dev.runwayml.com/');
|
|
21
|
+
}
|
|
22
|
+
const url = `${RUNWAY_API_BASE}${urlPath}`;
|
|
23
|
+
console.error(`[Runway API] ${options.method || 'GET'} ${url}`);
|
|
24
|
+
let response;
|
|
25
|
+
try {
|
|
26
|
+
response = await fetch(url, {
|
|
27
|
+
...options,
|
|
28
|
+
signal: options.signal ?? AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
29
|
+
headers: {
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
32
|
+
'X-Runway-Version': RUNWAY_API_VERSION,
|
|
33
|
+
...(options.headers || {}),
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error instanceof Error && error.name === 'TimeoutError') {
|
|
39
|
+
throw new RunwayError('Request to Runway API timed out', 'TIMEOUT', 'The request took too long. Try again or check if the Runway API is available.');
|
|
40
|
+
}
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
if (response.status === 429) {
|
|
44
|
+
throw new RunwayError('Rate limited.', 'RATE_LIMITED', 'Wait a moment and try again.');
|
|
45
|
+
}
|
|
46
|
+
if (response.status === 401) {
|
|
47
|
+
throw new RunwayError('Authentication failed.', 'AUTH_FAILED', 'Check your Runway API key.');
|
|
48
|
+
}
|
|
49
|
+
if (response.status === 403) {
|
|
50
|
+
throw new RunwayError('Access forbidden.', 'AUTH_FAILED', 'Check your Runway API key and account permissions.');
|
|
51
|
+
}
|
|
52
|
+
if (response.status === 404) {
|
|
53
|
+
throw new RunwayError('Resource not found.', 'NOT_FOUND', 'The resource does not exist or was deleted.');
|
|
54
|
+
}
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
let detail = '';
|
|
57
|
+
try {
|
|
58
|
+
detail = (await response.json())?.error || '';
|
|
59
|
+
}
|
|
60
|
+
catch { /* empty */ }
|
|
61
|
+
throw new RunwayError(`Runway API error (HTTP ${response.status}): ${detail}`, `HTTP_${response.status}`, 'Try again or check https://dev.runwayml.com/');
|
|
62
|
+
}
|
|
63
|
+
return (await response.json());
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Make a raw authenticated fetch (for DELETE endpoints that return 204 with no body).
|
|
67
|
+
*/
|
|
68
|
+
export async function runwayRawFetch(urlPath, options = {}) {
|
|
69
|
+
const apiKey = getApiKey();
|
|
70
|
+
if (!apiKey || apiKey.trim().length === 0) {
|
|
71
|
+
throw new RunwayError('Runway API key not configured', 'AUTH_REQUIRED', 'Configure your Runway API key in Settings. Get one at https://dev.runwayml.com/');
|
|
72
|
+
}
|
|
73
|
+
const url = `${RUNWAY_API_BASE}${urlPath}`;
|
|
74
|
+
console.error(`[Runway API] ${options.method || 'GET'} ${url}`);
|
|
75
|
+
let response;
|
|
76
|
+
try {
|
|
77
|
+
response = await fetch(url, {
|
|
78
|
+
...options,
|
|
79
|
+
signal: options.signal ?? AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
80
|
+
headers: {
|
|
81
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
82
|
+
'X-Runway-Version': RUNWAY_API_VERSION,
|
|
83
|
+
...(options.headers || {}),
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error instanceof Error && error.name === 'TimeoutError') {
|
|
89
|
+
throw new RunwayError('Request to Runway API timed out', 'TIMEOUT', 'The request took too long. Try again or check if the Runway API is available.');
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
if (response.status === 429) {
|
|
94
|
+
throw new RunwayError('Rate limited.', 'RATE_LIMITED', 'Wait a moment and try again.');
|
|
95
|
+
}
|
|
96
|
+
if (response.status === 401) {
|
|
97
|
+
throw new RunwayError('Authentication failed.', 'AUTH_FAILED', 'Check your Runway API key.');
|
|
98
|
+
}
|
|
99
|
+
if (response.status === 403) {
|
|
100
|
+
throw new RunwayError('Access forbidden.', 'AUTH_FAILED', 'Check your Runway API key and account permissions.');
|
|
101
|
+
}
|
|
102
|
+
return response;
|
|
103
|
+
}
|
|
104
|
+
// ============================================================================
|
|
105
|
+
// File resolution & ephemeral upload helpers
|
|
106
|
+
// ============================================================================
|
|
107
|
+
/**
|
|
108
|
+
* Upload a local file to Runway's ephemeral storage.
|
|
109
|
+
* Returns a runway:// URI valid for 24 hours.
|
|
110
|
+
*/
|
|
111
|
+
export async function uploadEphemeral(filePath) {
|
|
112
|
+
if (!fs.existsSync(filePath)) {
|
|
113
|
+
throw new RunwayError(`File not found: ${filePath}`, 'FILE_NOT_FOUND', 'Provide an accessible local file path.');
|
|
114
|
+
}
|
|
115
|
+
const stats = fs.statSync(filePath);
|
|
116
|
+
if (!stats.isFile()) {
|
|
117
|
+
throw new RunwayError(`Not a file: ${filePath}`, 'INVALID_INPUT', 'Provide a file path, not a directory.');
|
|
118
|
+
}
|
|
119
|
+
if (stats.size > MAX_UPLOAD_BYTES) {
|
|
120
|
+
throw new RunwayError('File exceeds 200MB upload limit.', 'FILE_TOO_LARGE', 'Reduce the file size or use a URL instead.');
|
|
121
|
+
}
|
|
122
|
+
if (stats.size < MIN_UPLOAD_BYTES) {
|
|
123
|
+
throw new RunwayError('File must be at least 512 bytes.', 'FILE_TOO_SMALL', 'Provide a valid media file.');
|
|
124
|
+
}
|
|
125
|
+
const filename = path.basename(filePath);
|
|
126
|
+
const uploadInfo = await runwayFetch('/uploads', {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
body: JSON.stringify({ filename, type: 'ephemeral' }),
|
|
129
|
+
});
|
|
130
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
131
|
+
const formData = new FormData();
|
|
132
|
+
for (const [key, value] of Object.entries(uploadInfo.fields)) {
|
|
133
|
+
formData.append(key, value);
|
|
134
|
+
}
|
|
135
|
+
formData.append('file', new Blob([fileBuffer]), filename);
|
|
136
|
+
const uploadRes = await fetch(uploadInfo.uploadUrl, { method: 'POST', body: formData });
|
|
137
|
+
if (!uploadRes.ok && uploadRes.status !== 204) {
|
|
138
|
+
throw new RunwayError(`Upload failed (HTTP ${uploadRes.status})`, 'UPLOAD_FAILED', 'Try again. If the file is too large (max 200MB), reduce its size.');
|
|
139
|
+
}
|
|
140
|
+
return uploadInfo.runwayUri;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Resolve a media input to a usable URI.
|
|
144
|
+
* - HTTPS/data/runway URIs are passed through.
|
|
145
|
+
* - Local files under the size limit are converted to data URIs.
|
|
146
|
+
* - Large local files are uploaded via ephemeral upload.
|
|
147
|
+
*/
|
|
148
|
+
export async function resolveMediaInput(input, category) {
|
|
149
|
+
if (input.startsWith('https://') || input.startsWith('data:') || input.startsWith('runway://')) {
|
|
150
|
+
return input;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const stats = fs.statSync(input);
|
|
154
|
+
if (!stats.isFile()) {
|
|
155
|
+
throw new RunwayError(`Not a file: ${input}`, 'INVALID_INPUT', 'Provide a file path, not a directory.');
|
|
156
|
+
}
|
|
157
|
+
const limit = DATA_URI_BINARY_LIMITS[category];
|
|
158
|
+
if (stats.size > limit) {
|
|
159
|
+
return await uploadEphemeral(input);
|
|
160
|
+
}
|
|
161
|
+
const buffer = fs.readFileSync(input);
|
|
162
|
+
const ext = input.split('.').pop()?.toLowerCase() || 'bin';
|
|
163
|
+
const fallback = category === 'image' ? 'image/png' : category === 'video' ? 'video/mp4' : 'audio/mpeg';
|
|
164
|
+
const mime = MIME_MAP[ext] || fallback;
|
|
165
|
+
return `data:${mime};base64,${buffer.toString('base64')}`;
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
if (err instanceof RunwayError)
|
|
169
|
+
throw err;
|
|
170
|
+
throw new RunwayError(`Could not read file: ${input}`, 'FILE_NOT_FOUND', 'Provide a valid HTTPS URL, Runway URI, or accessible local file path.');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// ============================================================================
|
|
174
|
+
// SSRF / Host validation for download URLs
|
|
175
|
+
// ============================================================================
|
|
176
|
+
/**
|
|
177
|
+
* Check whether a hostname is private, localhost, or otherwise reserved.
|
|
178
|
+
* Matches the same patterns as Workday's SSRF prevention.
|
|
179
|
+
*/
|
|
180
|
+
function isPrivateOrReservedHost(hostname) {
|
|
181
|
+
const lower = hostname.toLowerCase();
|
|
182
|
+
// Localhost names
|
|
183
|
+
if (lower === 'localhost' || lower === '[::1]' || lower === '::1') {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
// .local domains
|
|
187
|
+
if (lower.endsWith('.local')) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
// IPv4 private/reserved ranges
|
|
191
|
+
const ipMatch = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
192
|
+
if (ipMatch) {
|
|
193
|
+
const [, a, b] = ipMatch.map(Number);
|
|
194
|
+
if (a === 127)
|
|
195
|
+
return true; // 127.0.0.0/8 loopback
|
|
196
|
+
if (a === 10)
|
|
197
|
+
return true; // 10.0.0.0/8 private
|
|
198
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
199
|
+
return true; // 172.16.0.0/12 private
|
|
200
|
+
if (a === 192 && b === 168)
|
|
201
|
+
return true; // 192.168.0.0/16 private
|
|
202
|
+
if (a === 169 && b === 254)
|
|
203
|
+
return true; // 169.254.0.0/16 link-local
|
|
204
|
+
if (a === 0)
|
|
205
|
+
return true; // 0.0.0.0/8
|
|
206
|
+
}
|
|
207
|
+
// IPv6 private/loopback (bracket-wrapped from URL parsing)
|
|
208
|
+
if (lower.startsWith('[') && lower.endsWith(']')) {
|
|
209
|
+
const inner = lower.slice(1, -1);
|
|
210
|
+
if (inner === '::1' || inner === '::' || inner.startsWith('fe80:') || inner.startsWith('fc') || inner.startsWith('fd')) {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Validate a download URL for SSRF safety.
|
|
218
|
+
* Returns an error message if the URL is unsafe, or null if OK.
|
|
219
|
+
*/
|
|
220
|
+
export function validateDownloadUrl(url) {
|
|
221
|
+
let parsed;
|
|
222
|
+
try {
|
|
223
|
+
parsed = new URL(url);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return 'Invalid URL.';
|
|
227
|
+
}
|
|
228
|
+
if (parsed.protocol !== 'https:') {
|
|
229
|
+
return 'Only HTTPS URLs are supported for download.';
|
|
230
|
+
}
|
|
231
|
+
if (isPrivateOrReservedHost(parsed.hostname)) {
|
|
232
|
+
return 'Cannot download from local/private network addresses.';
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
// ============================================================================
|
|
237
|
+
// Cost estimation helper
|
|
238
|
+
// ============================================================================
|
|
239
|
+
export function costEstimate(model, duration, audio) {
|
|
240
|
+
const rates = {
|
|
241
|
+
'gen4.5': 12,
|
|
242
|
+
gen4_turbo: 5,
|
|
243
|
+
gen3a_turbo: 5,
|
|
244
|
+
gen4_aleph: 15,
|
|
245
|
+
act_two: 5,
|
|
246
|
+
veo3: 40,
|
|
247
|
+
'veo3.1': audio === false ? 20 : 40,
|
|
248
|
+
'veo3.1_fast': audio === false ? 10 : 15,
|
|
249
|
+
};
|
|
250
|
+
const credits = (rates[model] || 5) * duration;
|
|
251
|
+
return { credits, usd: `$${(credits * 0.01).toFixed(2)}` };
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Add content moderation to a request body if specified.
|
|
255
|
+
*/
|
|
256
|
+
export function addContentModeration(body, contentMod) {
|
|
257
|
+
if (contentMod === 'low') {
|
|
258
|
+
body.contentModeration = { publicFigureThreshold: 'low' };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
//# sourceMappingURL=client.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Runway ML MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Comprehensive AI media generation via Runway's API:
|
|
6
|
+
* - Video generation (text-to-video, image-to-video, video-to-video, character performance)
|
|
7
|
+
* - Image generation (text+reference-to-image)
|
|
8
|
+
* - Audio (TTS, sound effects, voice swap, dubbing, voice isolation)
|
|
9
|
+
* - Custom voices (list, create, preview, delete)
|
|
10
|
+
* - Task management (check, wait, cancel, download)
|
|
11
|
+
* - Account (balance, usage analytics, API key configuration)
|
|
12
|
+
*
|
|
13
|
+
* Environment variables:
|
|
14
|
+
* - RUNWAYML_API_SECRET: Runway API key (required)
|
|
15
|
+
* - MCP_HOST_BRIDGE_STATE: Path to host app bridge state file (optional)
|
|
16
|
+
* - MINDSTONE_REBEL_BRIDGE_STATE: Legacy bridge state path (optional)
|
|
17
|
+
*/
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Runway ML MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Comprehensive AI media generation via Runway's API:
|
|
6
|
+
* - Video generation (text-to-video, image-to-video, video-to-video, character performance)
|
|
7
|
+
* - Image generation (text+reference-to-image)
|
|
8
|
+
* - Audio (TTS, sound effects, voice swap, dubbing, voice isolation)
|
|
9
|
+
* - Custom voices (list, create, preview, delete)
|
|
10
|
+
* - Task management (check, wait, cancel, download)
|
|
11
|
+
* - Account (balance, usage analytics, API key configuration)
|
|
12
|
+
*
|
|
13
|
+
* Environment variables:
|
|
14
|
+
* - RUNWAYML_API_SECRET: Runway API key (required)
|
|
15
|
+
* - MCP_HOST_BRIDGE_STATE: Path to host app bridge state file (optional)
|
|
16
|
+
* - MINDSTONE_REBEL_BRIDGE_STATE: Legacy bridge state path (optional)
|
|
17
|
+
*/
|
|
18
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
19
|
+
import { createServer } from './server.js';
|
|
20
|
+
async function main() {
|
|
21
|
+
const server = createServer();
|
|
22
|
+
const transport = new StdioServerTransport();
|
|
23
|
+
await server.connect(transport);
|
|
24
|
+
console.error('Runway MCP server running on stdio');
|
|
25
|
+
}
|
|
26
|
+
main().catch((error) => {
|
|
27
|
+
console.error('Fatal error:', error);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
});
|
|
30
|
+
//# sourceMappingURL=index.js.map
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { registerConfigureTools, registerVideoTools, registerImageTools, registerAudioTools, registerVoiceTools, registerTaskTools, registerAccountTools, } from './tools/index.js';
|
|
3
|
+
export function createServer() {
|
|
4
|
+
const server = new McpServer({
|
|
5
|
+
name: 'runway-mcp-server',
|
|
6
|
+
version: '0.1.0',
|
|
7
|
+
});
|
|
8
|
+
registerConfigureTools(server);
|
|
9
|
+
registerVideoTools(server);
|
|
10
|
+
registerImageTools(server);
|
|
11
|
+
registerAudioTools(server);
|
|
12
|
+
registerVoiceTools(server);
|
|
13
|
+
registerTaskTools(server);
|
|
14
|
+
registerAccountTools(server);
|
|
15
|
+
return server;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { runwayFetch } from '../client.js';
|
|
3
|
+
import { withErrorHandling } from '../utils.js';
|
|
4
|
+
export function registerAccountTools(server) {
|
|
5
|
+
// ── Balance ───────────────────────────────────────────────────────────
|
|
6
|
+
server.registerTool('get_runway_balance', {
|
|
7
|
+
description: 'Check your Runway credit balance, usage tier limits, and today\'s usage by model. 1 credit = $0.01.',
|
|
8
|
+
inputSchema: z.object({}),
|
|
9
|
+
annotations: { readOnlyHint: true },
|
|
10
|
+
}, withErrorHandling(async () => {
|
|
11
|
+
const org = await runwayFetch('/organization');
|
|
12
|
+
const bal = org.creditBalance;
|
|
13
|
+
const activeModels = Object.entries(org.usage.models).filter(([, v]) => v.dailyGenerations > 0);
|
|
14
|
+
const summary = [
|
|
15
|
+
`Credit Balance: ${bal} credits ($${(bal * 0.01).toFixed(2)})`,
|
|
16
|
+
`Monthly Limit: ${org.tier.maxMonthlyCreditSpend} credits`,
|
|
17
|
+
'',
|
|
18
|
+
'Model Limits:',
|
|
19
|
+
...Object.entries(org.tier.models).map(([m, l]) => ` ${m}: ${l.maxConcurrentGenerations} concurrent, ${l.maxDailyGenerations}/day`),
|
|
20
|
+
...(activeModels.length
|
|
21
|
+
? ['', "Today's Usage:", ...activeModels.map(([m, v]) => ` ${m}: ${v.dailyGenerations} generations`)]
|
|
22
|
+
: []),
|
|
23
|
+
...(bal === 0 ? ['', 'WARNING: No credits. Add at https://dev.runwayml.com/ (Billing tab, min $10).'] : []),
|
|
24
|
+
];
|
|
25
|
+
return JSON.stringify({
|
|
26
|
+
ok: true, balance: bal, balance_usd: `$${(bal * 0.01).toFixed(2)}`,
|
|
27
|
+
summary: summary.join('\n'),
|
|
28
|
+
});
|
|
29
|
+
}));
|
|
30
|
+
// ── Credit Usage Analytics ────────────────────────────────────────────
|
|
31
|
+
server.registerTool('query_credit_usage', {
|
|
32
|
+
description: 'Query detailed credit usage broken down by model and day. Supports date ranges up to 90 days.',
|
|
33
|
+
inputSchema: z.object({
|
|
34
|
+
start_date: z.string().optional().describe('Start date (YYYY-MM-DD). Default: 30 days ago.'),
|
|
35
|
+
before_date: z.string().optional().describe('End date, not inclusive (YYYY-MM-DD). Default: 30 days after start.'),
|
|
36
|
+
}),
|
|
37
|
+
annotations: { readOnlyHint: true },
|
|
38
|
+
}, withErrorHandling(async (args) => {
|
|
39
|
+
const body = {};
|
|
40
|
+
if (args.start_date)
|
|
41
|
+
body.startDate = args.start_date;
|
|
42
|
+
if (args.before_date)
|
|
43
|
+
body.beforeDate = args.before_date;
|
|
44
|
+
const result = await runwayFetch('/organization/usage', {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
body: JSON.stringify(body),
|
|
47
|
+
});
|
|
48
|
+
let totalCredits = 0;
|
|
49
|
+
const byModel = {};
|
|
50
|
+
for (const day of result.results) {
|
|
51
|
+
for (const entry of day.usedCredits) {
|
|
52
|
+
totalCredits += entry.amount;
|
|
53
|
+
byModel[entry.model] = (byModel[entry.model] || 0) + entry.amount;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const lines = [
|
|
57
|
+
`Total credits used: ${totalCredits} ($${(totalCredits * 0.01).toFixed(2)})`,
|
|
58
|
+
`Period: ${result.results.length} days`,
|
|
59
|
+
'',
|
|
60
|
+
'By model:',
|
|
61
|
+
...Object.entries(byModel).sort(([, a], [, b]) => b - a)
|
|
62
|
+
.map(([m, c]) => ` ${m}: ${c} credits ($${(c * 0.01).toFixed(2)})`),
|
|
63
|
+
];
|
|
64
|
+
return JSON.stringify({
|
|
65
|
+
ok: true, total_credits: totalCredits,
|
|
66
|
+
days: result.results.length, by_model: byModel,
|
|
67
|
+
summary: lines.join('\n'),
|
|
68
|
+
});
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=account.js.map
|