@ronana/mcp 0.2.0 → 0.3.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/LICENSE +21 -0
- package/README.md +20 -5
- package/dist/cli.js +40 -19
- package/dist/input.js +4 -2
- package/dist/oauth.js +40 -12
- package/dist/remote.js +15 -3
- package/dist/server.js +38 -6
- package/package.json +32 -7
- package/dist/client.d.ts +0 -61
- package/dist/client.js +0 -128
- package/dist/schemas.d.ts +0 -101
- package/dist/schemas.js +0 -55
- package/dist/types.d.ts +0 -173
- package/dist/types.js +0 -11
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ronana
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# Ronana CLI and MCP
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require a paid plan.
|
|
3
|
+
Requires a Ronana paid plan.
|
|
5
4
|
|
|
6
5
|
WARNING: Generations spend your account credits.
|
|
7
6
|
|
|
@@ -39,7 +38,6 @@ ronana generate create google/nano-banana-2-lite/edit --prompt "make it dusk" --
|
|
|
39
38
|
ronana generate get <id> | wait <id> [--out dir]
|
|
40
39
|
ronana generate list
|
|
41
40
|
ronana upscale <id | file | url> --resolution 4k --out ./renders
|
|
42
|
-
ronana improve google/nano-banana-2-lite/edit --prompt "make it dusk"
|
|
43
41
|
ronana folder list | add <folder> <id…>
|
|
44
42
|
```
|
|
45
43
|
|
|
@@ -84,9 +82,27 @@ add it to the app's MCP settings:
|
|
|
84
82
|
}
|
|
85
83
|
```
|
|
86
84
|
|
|
85
|
+
On Windows, start it through `cmd`, or the app cannot find `npx`:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"mcpServers": {
|
|
90
|
+
"ronana": {
|
|
91
|
+
"command": "cmd",
|
|
92
|
+
"args": ["/c", "npx", "-y", "-p", "@ronana/mcp", "ronana-mcp"],
|
|
93
|
+
"env": { "RONANA_MCP_UPLOAD_ROOT": "C:\\Users\\you\\Pictures\\ronana" }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
In Claude Code: `claude mcp add ronana -- cmd /c npx -y -p @ronana/mcp ronana-mcp`.
|
|
100
|
+
In PowerShell, quote the separator (`'--'`), or PowerShell drops it.
|
|
101
|
+
|
|
87
102
|
The local server adds one tool, `upload_local_reference`, which uploads a file
|
|
88
103
|
from inside `RONANA_MCP_UPLOAD_ROOT`. Set it to the folder that holds your
|
|
89
|
-
media
|
|
104
|
+
media. The tool is off until it is set, and it refuses a home directory or a
|
|
105
|
+
drive root. Files larger than 3 MB stream
|
|
90
106
|
straight to storage, and Ronana checks each file before it adds it to your
|
|
91
107
|
library.
|
|
92
108
|
|
|
@@ -100,7 +116,6 @@ library.
|
|
|
100
116
|
| `generate` | Start an image or video generation. |
|
|
101
117
|
| `get_generation` | Get one generation and its download link. Pass `wait_seconds` (up to 45) to wait for the result in one call. |
|
|
102
118
|
| `upscale` | Upscale a video to a higher resolution. |
|
|
103
|
-
| `improve_prompt` | Rewrite a prompt so it works better on one model. |
|
|
104
119
|
| `list_generations` | List recent generations. |
|
|
105
120
|
| `list_references` | List library references. |
|
|
106
121
|
| `upload_reference` | Add a file to the library from a URL (up to 25 MB) or from base64 (under 3 MB). |
|
package/dist/cli.js
CHANGED
|
@@ -13,10 +13,9 @@
|
|
|
13
13
|
// ronana generate get | wait <id> [--out dir]
|
|
14
14
|
// ronana generate list [--limit n]
|
|
15
15
|
// ronana upscale <id | file | https-url> [--resolution r] [--model m] [--wait] [--out dir]
|
|
16
|
-
// ronana improve <model> --prompt "…"
|
|
17
16
|
// ronana folder list | add <folder> <id…>
|
|
18
17
|
import { randomUUID } from "node:crypto";
|
|
19
|
-
import { createWriteStream } from "node:fs";
|
|
18
|
+
import { createWriteStream, existsSync, statSync } from "node:fs";
|
|
20
19
|
import { mkdir } from "node:fs/promises";
|
|
21
20
|
import { basename, extname, join, resolve } from "node:path";
|
|
22
21
|
import { Readable } from "node:stream";
|
|
@@ -39,7 +38,6 @@ const HELP = `Usage: ronana <command>
|
|
|
39
38
|
generate wait <id> Wait for a generation to finish
|
|
40
39
|
generate list Recent generations (--limit n)
|
|
41
40
|
upscale <id|file|url> Upscale a video (costs credits; --resolution, --model)
|
|
42
|
-
improve <model> --prompt "…" Rewrite a prompt for a model (costs a little)
|
|
43
41
|
folder list Your library folders
|
|
44
42
|
folder add <folder> <id…> File items into a folder (made if missing)
|
|
45
43
|
|
|
@@ -72,6 +70,8 @@ function describeField(name, schema, required) {
|
|
|
72
70
|
parts.push(`default ${JSON.stringify(schema.default)}`);
|
|
73
71
|
return ` --${name.padEnd(22)} ${parts.join(" ")}${required ? " (required)" : ""}`;
|
|
74
72
|
}
|
|
73
|
+
/** Model ids are lowercase paths; forgive the casing and a trailing slash. */
|
|
74
|
+
const modelId = (raw) => raw.trim().replace(/\/+$/, "").toLowerCase();
|
|
75
75
|
const TERMINAL = new Set(["completed", "failed", "canceled"]);
|
|
76
76
|
function seconds(args, name, fallback) {
|
|
77
77
|
const raw = flag(args, name);
|
|
@@ -122,6 +122,8 @@ const EXT_BY_TYPE = {
|
|
|
122
122
|
"video/mp4": ".mp4", "video/webm": ".webm", "video/quicktime": ".mov",
|
|
123
123
|
};
|
|
124
124
|
async function download(gen, dir) {
|
|
125
|
+
if (existsSync(dir) && !statSync(dir).isDirectory())
|
|
126
|
+
throw new Error(`--out must be a folder; ${dir} is a file.`);
|
|
125
127
|
await mkdir(dir, { recursive: true });
|
|
126
128
|
const saved = [];
|
|
127
129
|
for (const output of gen.outputs) {
|
|
@@ -175,6 +177,8 @@ async function generateCommand(sub, args) {
|
|
|
175
177
|
const [target] = args.positional;
|
|
176
178
|
if (sub === "list") {
|
|
177
179
|
const limit = Number(flag(args, "limit") ?? 25);
|
|
180
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
181
|
+
throw new Error("--limit must be a whole number from 1 to 100.");
|
|
178
182
|
const rows = await withRemote((r) => callTool(r, "list_generations", { limit }));
|
|
179
183
|
return emit(rows, () => rows.forEach((g) => print(`${g.id.padEnd(28)} ${g.status.padEnd(11)} ${g.created_at}`)));
|
|
180
184
|
}
|
|
@@ -189,7 +193,7 @@ async function generateCommand(sub, args) {
|
|
|
189
193
|
if (sub !== "create" && sub !== "cost")
|
|
190
194
|
throw new Error("Use generate create | cost | get | wait | list.");
|
|
191
195
|
await withRemote(async (remote) => {
|
|
192
|
-
const contract = await callTool(remote, "get_model", { id: target });
|
|
196
|
+
const contract = await callTool(remote, "get_model", { id: modelId(target) });
|
|
193
197
|
const input = await buildInput(contract, args);
|
|
194
198
|
await replaceLocalMedia(contract, input, async (path) => {
|
|
195
199
|
note(`Uploading ${basename(path)}…`);
|
|
@@ -214,17 +218,23 @@ async function generateCommand(sub, args) {
|
|
|
214
218
|
async function modelCommand(sub, args) {
|
|
215
219
|
if (sub === "list" || sub === undefined) {
|
|
216
220
|
const kind = flag(args, "kind");
|
|
221
|
+
if (kind !== undefined && kind !== "image" && kind !== "video")
|
|
222
|
+
throw new Error("--kind must be image or video.");
|
|
217
223
|
const filter = args.positional[0]?.toLowerCase();
|
|
218
224
|
const models = (await withRemote((r) => callTool(r, "list_models", kind ? { kind } : {})))
|
|
219
225
|
.filter((m) => !filter || m.id.toLowerCase().includes(filter) || m.name.toLowerCase().includes(filter));
|
|
220
|
-
return emit(models, () =>
|
|
226
|
+
return emit(models, () => {
|
|
227
|
+
if (!models.length)
|
|
228
|
+
note(filter ? `No models match "${args.positional[0]}".` : "No models are available on your plan.");
|
|
229
|
+
models.forEach((m) => print(`${m.id.padEnd(52)} ${m.kind.padEnd(6)} ${m.name}${m.best_for ? ` ${m.best_for}` : ""}`));
|
|
230
|
+
});
|
|
221
231
|
}
|
|
222
232
|
if (sub !== "get")
|
|
223
233
|
throw new Error("Use model list | get.");
|
|
224
234
|
const id = args.positional[0];
|
|
225
235
|
if (!id)
|
|
226
236
|
throw new Error("Usage: ronana model get <model>");
|
|
227
|
-
const contract = await withRemote((r) => callTool(r, "get_model", { id }));
|
|
237
|
+
const contract = await withRemote((r) => callTool(r, "get_model", { id: modelId(id) }));
|
|
228
238
|
emit(contract, () => {
|
|
229
239
|
print(`${contract.name} (${contract.kind})`);
|
|
230
240
|
print(contract.id);
|
|
@@ -268,15 +278,26 @@ async function folderCommand(sub, args) {
|
|
|
268
278
|
if (sub !== "add")
|
|
269
279
|
throw new Error("Use folder list | add.");
|
|
270
280
|
const [folder, ...ids] = args.positional;
|
|
271
|
-
if (!folder || ids.length === 0)
|
|
281
|
+
if (!folder?.trim() || ids.length === 0)
|
|
272
282
|
throw new Error("Usage: ronana folder add <folder> <id…>");
|
|
283
|
+
if (folder.trim().length > 60)
|
|
284
|
+
throw new Error("Folder names are at most 60 characters.");
|
|
273
285
|
const filed = await withRemote((r) => callTool(r, "add_to_folder", { ids, folder, create: true }));
|
|
274
|
-
|
|
286
|
+
emit(filed, () => {
|
|
287
|
+
print(`Filed ${filed.filed.length} in "${filed.folder.name}".`);
|
|
288
|
+
if (filed.not_found?.length)
|
|
289
|
+
note(`Not found: ${filed.not_found.join(", ")}`);
|
|
290
|
+
});
|
|
291
|
+
if (filed.not_found?.length)
|
|
292
|
+
process.exitCode = 1;
|
|
275
293
|
}
|
|
276
294
|
async function main() {
|
|
277
|
-
const [command,
|
|
295
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
278
296
|
const takesSub = command === "auth" || command === "model" || command === "generate" || command === "folder";
|
|
279
|
-
const args = parseArgs(
|
|
297
|
+
const args = parseArgs(rest);
|
|
298
|
+
// The subcommand is the first word that is not a flag, wherever the flags
|
|
299
|
+
// sit (`ronana generate --json list`, `ronana generate --help`).
|
|
300
|
+
const sub = takesSub ? args.positional.shift() : undefined;
|
|
280
301
|
JSON_OUT = has(args, "json");
|
|
281
302
|
if (!command || ["help", "--help", "-h"].includes(command) || has(args, "help"))
|
|
282
303
|
return void process.stdout.write(HELP);
|
|
@@ -310,6 +331,8 @@ async function main() {
|
|
|
310
331
|
const source = args.positional[0];
|
|
311
332
|
if (!source)
|
|
312
333
|
throw new Error("Usage: ronana upload <file | https-url>");
|
|
334
|
+
if (/^http:\/\//i.test(source))
|
|
335
|
+
throw new Error("Use an https:// link; plain http is not accepted.");
|
|
313
336
|
const ref = await withRemote((r) => /^https:\/\//i.test(source)
|
|
314
337
|
? callTool(r, "upload_reference", { url: source, filename: basename(new URL(source).pathname) || "upload" })
|
|
315
338
|
: uploadFile(r, resolve(source)));
|
|
@@ -317,14 +340,6 @@ async function main() {
|
|
|
317
340
|
}
|
|
318
341
|
case "generate": return generateCommand(sub, args);
|
|
319
342
|
case "upscale": return upscaleCommand(args);
|
|
320
|
-
case "improve": {
|
|
321
|
-
const model = args.positional[0];
|
|
322
|
-
const prompt = flag(args, "prompt");
|
|
323
|
-
if (!model || !prompt)
|
|
324
|
-
throw new Error("Usage: ronana improve <model> --prompt \"…\"");
|
|
325
|
-
const improved = await withRemote((r) => callTool(r, "improve_prompt", { model, prompt }));
|
|
326
|
-
return emit(improved, () => print(improved.prompt));
|
|
327
|
-
}
|
|
328
343
|
case "folder":
|
|
329
344
|
case "folders": return folderCommand(command === "folders" ? "list" : sub, args);
|
|
330
345
|
default: throw new Error(`Unknown command "${command}". Run \`ronana --help\`.`);
|
|
@@ -332,7 +347,13 @@ async function main() {
|
|
|
332
347
|
}
|
|
333
348
|
main().catch((error) => {
|
|
334
349
|
if (error instanceof ToolError) {
|
|
335
|
-
const { code,
|
|
350
|
+
const { code, url, billing, field } = error.error;
|
|
351
|
+
let message = String(error.error.message ?? "The request failed.");
|
|
352
|
+
if (code === "MODEL_UNKNOWN")
|
|
353
|
+
message = "This model is not available. Run `ronana model list` for the current ids.";
|
|
354
|
+
// Name the field the server refused, unless the message already does.
|
|
355
|
+
else if (typeof field === "string" && !message.includes(field))
|
|
356
|
+
message = `${field}: ${message}`;
|
|
336
357
|
if (JSON_OUT)
|
|
337
358
|
process.stdout.write(`${JSON.stringify({ ok: false, error: error.error }, null, 2)}\n`);
|
|
338
359
|
else
|
package/dist/input.js
CHANGED
|
@@ -49,7 +49,7 @@ function coerce(field, raw, schema) {
|
|
|
49
49
|
if (type === "integer" || type === "number") {
|
|
50
50
|
const n = Number(raw);
|
|
51
51
|
if (!Number.isFinite(n) || (type === "integer" && !Number.isInteger(n)))
|
|
52
|
-
throw new Error(`--${field} must be
|
|
52
|
+
throw new Error(`--${field} must be ${type === "integer" ? "a whole number" : "a number"}.`);
|
|
53
53
|
return n;
|
|
54
54
|
}
|
|
55
55
|
if (type === "boolean") {
|
|
@@ -70,7 +70,9 @@ function coerce(field, raw, schema) {
|
|
|
70
70
|
async function readInputFlag(raw) {
|
|
71
71
|
if (!raw)
|
|
72
72
|
return {};
|
|
73
|
-
const text = raw.startsWith("@")
|
|
73
|
+
const text = raw.startsWith("@")
|
|
74
|
+
? await readFile(raw.slice(1), "utf8").catch(() => { throw new Error(`--input: cannot read ${raw.slice(1)}.`); })
|
|
75
|
+
: raw;
|
|
74
76
|
let parsed;
|
|
75
77
|
try {
|
|
76
78
|
parsed = JSON.parse(text);
|
package/dist/oauth.js
CHANGED
|
@@ -26,15 +26,19 @@ export function readCredentials() {
|
|
|
26
26
|
function writeCredentials(value) {
|
|
27
27
|
entry().setPassword(JSON.stringify(value));
|
|
28
28
|
}
|
|
29
|
+
/** Ends a stored sign-in on the server, so it leaves the account's connections.
|
|
30
|
+
* Best effort: an unreachable server still lets the local step go ahead. */
|
|
31
|
+
async function revoke(credentials) {
|
|
32
|
+
await fetch(`${credentials.baseUrl}/api/auth/oauth2/revoke`, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
35
|
+
body: new URLSearchParams({ token: credentials.refreshToken, client_id: credentials.clientId }),
|
|
36
|
+
}).catch(() => null);
|
|
37
|
+
}
|
|
29
38
|
export async function logout() {
|
|
30
39
|
const current = readCredentials();
|
|
31
|
-
if (current)
|
|
32
|
-
await
|
|
33
|
-
method: "POST",
|
|
34
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
35
|
-
body: new URLSearchParams({ token: current.refreshToken, client_id: current.clientId }),
|
|
36
|
-
}).catch(() => null);
|
|
37
|
-
}
|
|
40
|
+
if (current)
|
|
41
|
+
await revoke(current);
|
|
38
42
|
try {
|
|
39
43
|
entry().deletePassword();
|
|
40
44
|
}
|
|
@@ -64,6 +68,14 @@ async function tokenRequest(baseUrl, fields) {
|
|
|
64
68
|
}
|
|
65
69
|
export async function login(baseUrl = process.env.RONANA_BASE_URL ?? "https://ronana.ai", openInBrowser = true) {
|
|
66
70
|
baseUrl = baseUrl.replace(/\/$/, "");
|
|
71
|
+
const parsed = (() => { try {
|
|
72
|
+
return new URL(baseUrl);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
} })();
|
|
77
|
+
if (!parsed || !/^https?:$/.test(parsed.protocol))
|
|
78
|
+
throw new Error("--base-url must be a web address, like https://ronana.ai.");
|
|
67
79
|
const verifier = randomBytes(32).toString("base64url");
|
|
68
80
|
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
69
81
|
const state = randomBytes(24).toString("base64url");
|
|
@@ -75,11 +87,13 @@ export async function login(baseUrl = process.env.RONANA_BASE_URL ?? "https://ro
|
|
|
75
87
|
const close = () => { clearTimeout(timer); server.close(); };
|
|
76
88
|
const server = createServer((req, res) => {
|
|
77
89
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
// Anything but our own callback (a favicon, a prefetch, a stale tab from an
|
|
91
|
+
// earlier sign-in) is ignored, so it cannot end the sign-in it isn't part of.
|
|
92
|
+
if (url.pathname !== "/callback" || url.searchParams.get("state") !== state) {
|
|
93
|
+
res.writeHead(404).end();
|
|
94
|
+
return;
|
|
81
95
|
}
|
|
82
|
-
|
|
96
|
+
if (url.searchParams.get("error")) {
|
|
83
97
|
res.writeHead(400).end("Authorization was not completed.");
|
|
84
98
|
fail(new Error(url.searchParams.get("error_description") ?? url.searchParams.get("error")));
|
|
85
99
|
}
|
|
@@ -113,8 +127,10 @@ export async function login(baseUrl = process.env.RONANA_BASE_URL ?? "https://ro
|
|
|
113
127
|
response_types: ["code"],
|
|
114
128
|
scope: SCOPES,
|
|
115
129
|
}),
|
|
130
|
+
}).catch(() => { throw new Error(`Could not reach ${baseUrl}. Check --base-url and your connection.`); });
|
|
131
|
+
const registered = await registration.json().catch(() => {
|
|
132
|
+
throw new Error(`No Ronana sign-in at ${baseUrl}. Check --base-url.`);
|
|
116
133
|
});
|
|
117
|
-
const registered = await registration.json();
|
|
118
134
|
if (!registration.ok || typeof registered.client_id !== "string")
|
|
119
135
|
throw new Error("OAuth client registration failed.");
|
|
120
136
|
const authorize = new URL(`${baseUrl}/api/auth/oauth2/authorize`);
|
|
@@ -144,6 +160,9 @@ ${authorize}
|
|
|
144
160
|
});
|
|
145
161
|
if (typeof tokens.access_token !== "string" || typeof tokens.refresh_token !== "string")
|
|
146
162
|
throw new Error("OAuth response did not contain tokens.");
|
|
163
|
+
// Signing in again replaces the stored sign-in; end the old one too, or
|
|
164
|
+
// every login leaves another connection behind on the account.
|
|
165
|
+
const previous = readCredentials();
|
|
147
166
|
writeCredentials({
|
|
148
167
|
baseUrl,
|
|
149
168
|
clientId: registered.client_id,
|
|
@@ -151,6 +170,8 @@ ${authorize}
|
|
|
151
170
|
refreshToken: tokens.refresh_token,
|
|
152
171
|
expiresAt: Date.now() + Number(tokens.expires_in ?? 900) * 1000,
|
|
153
172
|
});
|
|
173
|
+
if (previous && previous.clientId !== registered.client_id)
|
|
174
|
+
await revoke(previous);
|
|
154
175
|
}
|
|
155
176
|
finally {
|
|
156
177
|
callback.close();
|
|
@@ -210,6 +231,13 @@ export async function accessToken() {
|
|
|
210
231
|
client_id: current.clientId,
|
|
211
232
|
refresh_token: current.refreshToken,
|
|
212
233
|
resource: `${current.baseUrl}/mcp`,
|
|
234
|
+
}).catch((error) => {
|
|
235
|
+
// A network failure (fetch's TypeError) may pass; a refusal means this
|
|
236
|
+
// sign-in is over (disconnected on the website, or expired), and the
|
|
237
|
+
// server's own words ("session not found") would read as something else.
|
|
238
|
+
if (error instanceof TypeError)
|
|
239
|
+
throw error;
|
|
240
|
+
throw new Error("Your Ronana sign-in has ended (disconnected or expired). Run `ronana auth login` again.");
|
|
213
241
|
});
|
|
214
242
|
if (typeof tokens.access_token !== "string")
|
|
215
243
|
throw new Error("OAuth refresh failed.");
|
package/dist/remote.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The one connection to Ronana's remote MCP endpoint, shared by the stdio
|
|
2
2
|
// bridge (server.ts) and the CLI (cli.ts). Both sign in once, with the same
|
|
3
3
|
// stored OAuth credential, and every call runs the server's own gates.
|
|
4
|
-
import { createReadStream, statSync } from "node:fs";
|
|
4
|
+
import { createReadStream, existsSync, statSync } from "node:fs";
|
|
5
5
|
import { readFile } from "node:fs/promises";
|
|
6
6
|
import { basename, extname } from "node:path";
|
|
7
7
|
import { Readable } from "node:stream";
|
|
@@ -27,7 +27,7 @@ export async function remoteClient() {
|
|
|
27
27
|
const advertised = await fetch(`${credentials.baseUrl}/.well-known/oauth-protected-resource/mcp`)
|
|
28
28
|
.then((r) => r.ok, () => false);
|
|
29
29
|
throw new Error(advertised
|
|
30
|
-
?
|
|
30
|
+
? `Ronana's CLI and MCP need a paid plan: ${credentials.baseUrl}/pricing`
|
|
31
31
|
: `No Ronana MCP endpoint at ${credentials.baseUrl}. Sign in again with the right --base-url.`);
|
|
32
32
|
}
|
|
33
33
|
throw error;
|
|
@@ -53,6 +53,11 @@ export async function callTool(remote, name, args = {}) {
|
|
|
53
53
|
if (body?.error)
|
|
54
54
|
throw new ToolError(body.error);
|
|
55
55
|
const text = Array.isArray(response.content) ? response.content.map((c) => ("text" in c ? c.text : "")).join(" ") : "";
|
|
56
|
+
// The SDK's own argument check answers in prose ("Input validation error:
|
|
57
|
+
// Invalid arguments for tool x: limit: Invalid input"); keep only the field.
|
|
58
|
+
const invalid = /Invalid arguments for tool \S+: ([\w.[\]-]+):/.exec(text);
|
|
59
|
+
if (invalid)
|
|
60
|
+
throw new ToolError({ code: "INVALID_REQUEST", message: `The value for "${invalid[1]}" is not valid.`, field: invalid[1] });
|
|
56
61
|
throw new ToolError({ code: "TOOL_FAILED", message: text || `${name} failed.` });
|
|
57
62
|
}
|
|
58
63
|
/* Must match upload_reference's `data` cap (3 MB of bytes ≈ 4.2 MB of base64,
|
|
@@ -67,7 +72,14 @@ const CONTENT_TYPES = {
|
|
|
67
72
|
* as base64; larger ones stream straight to storage through a signed PUT. */
|
|
68
73
|
export async function uploadFile(remote, path) {
|
|
69
74
|
const filename = basename(path);
|
|
70
|
-
|
|
75
|
+
if (!existsSync(path))
|
|
76
|
+
throw new Error(`No file at ${path}.`);
|
|
77
|
+
const stat = statSync(path);
|
|
78
|
+
if (!stat.isFile())
|
|
79
|
+
throw new Error(`${path} is a folder, not a file.`);
|
|
80
|
+
const { size } = stat;
|
|
81
|
+
if (size === 0)
|
|
82
|
+
throw new Error(`${filename} is empty.`);
|
|
71
83
|
if (size <= INLINE_LIMIT) {
|
|
72
84
|
const data = (await readFile(path)).toString("base64");
|
|
73
85
|
return callTool(remote, "upload_reference", { data, filename });
|
package/dist/server.js
CHANGED
|
@@ -1,15 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpath, stat } from "node:fs/promises";
|
|
3
|
-
import {
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { isAbsolute, parse, relative, resolve, sep } from "node:path";
|
|
4
5
|
import { fromJsonSchema, McpServer } from "@modelcontextprotocol/server";
|
|
5
6
|
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
6
7
|
import { z } from "zod4";
|
|
7
8
|
import { login, logout, readCredentials } from "./oauth.js";
|
|
8
9
|
import { remoteClient, ToolError, uploadFile } from "./remote.js";
|
|
9
|
-
|
|
10
|
+
/* Uploads are off until the user names a folder: defaulting to the working
|
|
11
|
+
directory handed an AI app whatever folder it happened to start in, often
|
|
12
|
+
the home folder. A home folder or a drive root is refused for the same
|
|
13
|
+
reason. */
|
|
14
|
+
const UPLOAD_ROOT = process.env.RONANA_MCP_UPLOAD_ROOT ? resolve(process.env.RONANA_MCP_UPLOAD_ROOT) : null;
|
|
10
15
|
async function resolveInsideRoot(inputPath) {
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
if (!UPLOAD_ROOT)
|
|
17
|
+
throw new Error("Local uploads are off. Set RONANA_MCP_UPLOAD_ROOT to the folder that holds your media.");
|
|
18
|
+
const root = await realpath(UPLOAD_ROOT).catch(() => {
|
|
19
|
+
throw new Error(`RONANA_MCP_UPLOAD_ROOT (${UPLOAD_ROOT}) does not exist.`);
|
|
20
|
+
});
|
|
21
|
+
// A root that holds the home folder (the home folder itself, C:\Users, /home
|
|
22
|
+
// or a drive root) would expose every file in it.
|
|
23
|
+
const home = await realpath(homedir()).catch(() => homedir());
|
|
24
|
+
const homeFromRoot = relative(root, home);
|
|
25
|
+
if (root === parse(root).root || homeFromRoot === "" || !(homeFromRoot.startsWith("..") || isAbsolute(homeFromRoot))) {
|
|
26
|
+
throw new Error("RONANA_MCP_UPLOAD_ROOT must be a media folder, not a home folder, a folder that contains one, or a drive root.");
|
|
27
|
+
}
|
|
28
|
+
const candidate = await realpath(isAbsolute(inputPath) ? inputPath : resolve(root, inputPath)).catch(() => {
|
|
29
|
+
throw new Error(`No file at ${inputPath} in RONANA_MCP_UPLOAD_ROOT.`);
|
|
30
|
+
});
|
|
13
31
|
const rel = relative(root, candidate);
|
|
14
32
|
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel))
|
|
15
33
|
throw new Error("The path is outside RONANA_MCP_UPLOAD_ROOT.");
|
|
@@ -18,8 +36,18 @@ async function resolveInsideRoot(inputPath) {
|
|
|
18
36
|
async function serve() {
|
|
19
37
|
const remote = await remoteClient();
|
|
20
38
|
const { tools } = await remote.listTools();
|
|
39
|
+
// Present as Ronana itself: the remote server's own title, logo and website,
|
|
40
|
+
// so an app lists this bridge with the Ronana mark rather than a bare name.
|
|
41
|
+
const remoteInfo = remote.getServerVersion();
|
|
42
|
+
const icons = remoteInfo?.icons;
|
|
21
43
|
serveStdio(() => {
|
|
22
|
-
const server = new McpServer({
|
|
44
|
+
const server = new McpServer({
|
|
45
|
+
name: "ronana",
|
|
46
|
+
title: remoteInfo?.title ?? "Ronana",
|
|
47
|
+
version: "1.0.0",
|
|
48
|
+
...(remoteInfo?.websiteUrl ? { websiteUrl: remoteInfo.websiteUrl } : {}),
|
|
49
|
+
...(icons ? { icons } : {}),
|
|
50
|
+
});
|
|
23
51
|
for (const tool of tools) {
|
|
24
52
|
server.registerTool(tool.name, {
|
|
25
53
|
title: tool.title,
|
|
@@ -27,15 +55,19 @@ async function serve() {
|
|
|
27
55
|
inputSchema: fromJsonSchema(tool.inputSchema),
|
|
28
56
|
outputSchema: tool.outputSchema ? fromJsonSchema(tool.outputSchema) : undefined,
|
|
29
57
|
annotations: tool.annotations,
|
|
58
|
+
...(tool.icons ? { icons: tool.icons } : {}),
|
|
30
59
|
}, async (args) => remote.callTool({
|
|
31
60
|
name: tool.name,
|
|
32
61
|
arguments: args,
|
|
33
62
|
}));
|
|
34
63
|
}
|
|
35
64
|
server.registerTool("upload_local_reference", {
|
|
36
|
-
description:
|
|
65
|
+
description: UPLOAD_ROOT
|
|
66
|
+
? `Upload a local file below ${UPLOAD_ROOT} to the account library.`
|
|
67
|
+
: "Upload a local file to the account library. Off until RONANA_MCP_UPLOAD_ROOT names a media folder.",
|
|
37
68
|
inputSchema: z.object({ path: z.string() }),
|
|
38
69
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
70
|
+
...(icons ? { icons } : {}),
|
|
39
71
|
}, async ({ path }) => {
|
|
40
72
|
try {
|
|
41
73
|
const safePath = await resolveInsideRoot(path);
|
package/package.json
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ronana/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Ronana command line and local MCP bridge.",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "MIT",
|
|
6
6
|
"homepage": "https://ronana.ai",
|
|
7
|
-
"
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/ronana-ai/cli.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/ronana-ai/cli/issues"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"ronana",
|
|
16
|
+
"mcp",
|
|
17
|
+
"cli",
|
|
18
|
+
"image generation",
|
|
19
|
+
"video generation"
|
|
20
|
+
],
|
|
8
21
|
"type": "module",
|
|
9
|
-
"bin": {
|
|
10
|
-
|
|
11
|
-
|
|
22
|
+
"bin": {
|
|
23
|
+
"ronana": "dist/cli.js",
|
|
24
|
+
"ronana-mcp": "dist/server.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public",
|
|
32
|
+
"provenance": true
|
|
33
|
+
},
|
|
12
34
|
"scripts": {
|
|
13
35
|
"build": "tsc -p tsconfig.json",
|
|
14
36
|
"prepublishOnly": "npm run build",
|
|
@@ -21,8 +43,11 @@
|
|
|
21
43
|
"zod4": "npm:zod@4.4.3"
|
|
22
44
|
},
|
|
23
45
|
"devDependencies": {
|
|
46
|
+
"@types/node": "^22.10.5",
|
|
24
47
|
"typescript": "^5",
|
|
25
48
|
"tsx": "^4"
|
|
26
49
|
},
|
|
27
|
-
"engines": {
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=20"
|
|
52
|
+
}
|
|
28
53
|
}
|
package/dist/client.d.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import type { ApiErrorBody, ApiPublicModel, GenerationState } from "./types.js";
|
|
2
|
-
export declare class RonanaApiError extends Error {
|
|
3
|
-
readonly code: string;
|
|
4
|
-
readonly status: number;
|
|
5
|
-
readonly body: ApiErrorBody["error"] | null;
|
|
6
|
-
constructor(code: string, message: string, status: number, body: ApiErrorBody["error"] | null);
|
|
7
|
-
/** A sentence a person or an agent can act on. The `action` field is the
|
|
8
|
-
* server's own decision about which door to open, so this never guesses. */
|
|
9
|
-
get guidance(): string;
|
|
10
|
-
}
|
|
11
|
-
export interface ClientOptions {
|
|
12
|
-
apiKey: string;
|
|
13
|
-
baseUrl: string;
|
|
14
|
-
}
|
|
15
|
-
/** Read the configuration from the environment. Throws with a clear message
|
|
16
|
-
* when the key is missing, because that is the one mistake every new user
|
|
17
|
-
* makes. */
|
|
18
|
-
export declare function clientOptionsFromEnv(env?: NodeJS.ProcessEnv): ClientOptions;
|
|
19
|
-
export declare class RonanaClient {
|
|
20
|
-
private readonly options;
|
|
21
|
-
constructor(options: ClientOptions);
|
|
22
|
-
private request;
|
|
23
|
-
listModels(): Promise<{
|
|
24
|
-
models: ApiPublicModel[];
|
|
25
|
-
tier: string;
|
|
26
|
-
}>;
|
|
27
|
-
account(): Promise<unknown>;
|
|
28
|
-
quote(input: Record<string, unknown>): Promise<{
|
|
29
|
-
available: boolean;
|
|
30
|
-
credits: number;
|
|
31
|
-
}>;
|
|
32
|
-
submitVideo(body: Record<string, unknown>): Promise<{
|
|
33
|
-
id: string;
|
|
34
|
-
status: string;
|
|
35
|
-
}>;
|
|
36
|
-
submitImage(body: Record<string, unknown>): Promise<{
|
|
37
|
-
id: string;
|
|
38
|
-
status: string;
|
|
39
|
-
}>;
|
|
40
|
-
generation(id: string): Promise<GenerationState>;
|
|
41
|
-
/** Upload a file inline. Use this up to 4 MB; larger files need the presigned
|
|
42
|
-
* flow below, because a serverless function body cannot exceed 4.5 MB. */
|
|
43
|
-
uploadInline(bytes: Uint8Array, filename: string): Promise<{
|
|
44
|
-
ref: {
|
|
45
|
-
id: string;
|
|
46
|
-
};
|
|
47
|
-
reused?: boolean;
|
|
48
|
-
}>;
|
|
49
|
-
/** Upload a file larger than 4 MB: sign, PUT straight to storage, finalize.
|
|
50
|
-
*
|
|
51
|
-
* CAUTION: The PUT must send exactly the content type and the content length
|
|
52
|
-
* that were signed. Any other value fails the storage signature check. */
|
|
53
|
-
uploadPresigned(bytes: Uint8Array, filename: string, contentType: string): Promise<{
|
|
54
|
-
ref: {
|
|
55
|
-
id: string;
|
|
56
|
-
};
|
|
57
|
-
reused?: boolean;
|
|
58
|
-
}>;
|
|
59
|
-
}
|
|
60
|
-
/** The most bytes an inline upload may carry. Above this, use the signed flow. */
|
|
61
|
-
export declare const INLINE_UPLOAD_MAX_BYTES: number;
|
package/dist/client.js
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
// The HTTP client for the RonanaLive public API.
|
|
2
|
-
//
|
|
3
|
-
// THE LOAD-BEARING RULE OF THIS PACKAGE: it is a thin HTTP client of
|
|
4
|
-
// `/api/v1/*`. It imports no database, no credit module, and no ORM. If it ever
|
|
5
|
-
// imported the application's database layer, somebody would eventually run this
|
|
6
|
-
// package against production.
|
|
7
|
-
//
|
|
8
|
-
// Everything it needs is two environment variables:
|
|
9
|
-
// RONANA_API_KEY the key, `rk_…`, minted at /developers/keys
|
|
10
|
-
// RONANA_BASE_URL the origin, default https://ronana.ai
|
|
11
|
-
export class RonanaApiError extends Error {
|
|
12
|
-
code;
|
|
13
|
-
status;
|
|
14
|
-
body;
|
|
15
|
-
constructor(code, message, status, body) {
|
|
16
|
-
super(message);
|
|
17
|
-
this.code = code;
|
|
18
|
-
this.status = status;
|
|
19
|
-
this.body = body;
|
|
20
|
-
this.name = "RonanaApiError";
|
|
21
|
-
}
|
|
22
|
-
/** A sentence a person or an agent can act on. The `action` field is the
|
|
23
|
-
* server's own decision about which door to open, so this never guesses. */
|
|
24
|
-
get guidance() {
|
|
25
|
-
const billing = this.body?.billing;
|
|
26
|
-
if (!billing)
|
|
27
|
-
return this.message;
|
|
28
|
-
switch (billing.action) {
|
|
29
|
-
case "buy_credits": return `${this.message} Buy more credits at ${billing.url}.`;
|
|
30
|
-
case "upgrade_plan": return `${this.message} Move to a larger plan at ${billing.url}.`;
|
|
31
|
-
case "either": return `${this.message} Buy credits or move to a larger plan at ${billing.url}.`;
|
|
32
|
-
case "verify_email": return `${this.message} Verify the account email at ${billing.url}.`;
|
|
33
|
-
case "wait": return `${this.message} Wait, then try again.`;
|
|
34
|
-
case "contact_support": return `${this.message} Contact support.`;
|
|
35
|
-
default: return this.message;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
/** Read the configuration from the environment. Throws with a clear message
|
|
40
|
-
* when the key is missing, because that is the one mistake every new user
|
|
41
|
-
* makes. */
|
|
42
|
-
export function clientOptionsFromEnv(env = process.env) {
|
|
43
|
-
const apiKey = env.RONANA_API_KEY?.trim();
|
|
44
|
-
if (!apiKey) {
|
|
45
|
-
throw new Error("RONANA_API_KEY is not set. Create a key at https://ronana.ai/developers/keys, " +
|
|
46
|
-
"then put it in your MCP client configuration as RONANA_API_KEY.");
|
|
47
|
-
}
|
|
48
|
-
const baseUrl = (env.RONANA_BASE_URL?.trim() || "https://ronana.ai").replace(/\/+$/, "");
|
|
49
|
-
return { apiKey, baseUrl };
|
|
50
|
-
}
|
|
51
|
-
export class RonanaClient {
|
|
52
|
-
options;
|
|
53
|
-
constructor(options) {
|
|
54
|
-
this.options = options;
|
|
55
|
-
}
|
|
56
|
-
async request(method, path, body) {
|
|
57
|
-
const res = await fetch(`${this.options.baseUrl}${path}`, {
|
|
58
|
-
method,
|
|
59
|
-
headers: {
|
|
60
|
-
authorization: `Bearer ${this.options.apiKey}`,
|
|
61
|
-
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
62
|
-
},
|
|
63
|
-
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
64
|
-
});
|
|
65
|
-
const text = await res.text();
|
|
66
|
-
const parsed = text ? JSON.parse(text) : null;
|
|
67
|
-
if (!res.ok) {
|
|
68
|
-
const envelope = parsed?.error ?? null;
|
|
69
|
-
throw new RonanaApiError(envelope?.code ?? "UNKNOWN", envelope?.message ?? `Request failed with status ${res.status}.`, res.status, envelope);
|
|
70
|
-
}
|
|
71
|
-
return parsed;
|
|
72
|
-
}
|
|
73
|
-
listModels() {
|
|
74
|
-
return this.request("GET", "/api/v1/models");
|
|
75
|
-
}
|
|
76
|
-
account() {
|
|
77
|
-
return this.request("GET", "/api/v1/me");
|
|
78
|
-
}
|
|
79
|
-
quote(input) {
|
|
80
|
-
return this.request("POST", "/api/v1/pricing/quote", input);
|
|
81
|
-
}
|
|
82
|
-
submitVideo(body) {
|
|
83
|
-
return this.request("POST", "/api/v1/generations", body);
|
|
84
|
-
}
|
|
85
|
-
submitImage(body) {
|
|
86
|
-
return this.request("POST", "/api/v1/images", body);
|
|
87
|
-
}
|
|
88
|
-
generation(id) {
|
|
89
|
-
return this.request("GET", `/api/v1/generations/${encodeURIComponent(id)}`);
|
|
90
|
-
}
|
|
91
|
-
/** Upload a file inline. Use this up to 4 MB; larger files need the presigned
|
|
92
|
-
* flow below, because a serverless function body cannot exceed 4.5 MB. */
|
|
93
|
-
async uploadInline(bytes, filename) {
|
|
94
|
-
const res = await fetch(`${this.options.baseUrl}/api/v1/refs`, {
|
|
95
|
-
method: "POST",
|
|
96
|
-
headers: {
|
|
97
|
-
authorization: `Bearer ${this.options.apiKey}`,
|
|
98
|
-
"content-type": "application/octet-stream",
|
|
99
|
-
"x-filename": encodeURIComponent(filename),
|
|
100
|
-
},
|
|
101
|
-
body: bytes,
|
|
102
|
-
});
|
|
103
|
-
const text = await res.text();
|
|
104
|
-
const parsed = text ? JSON.parse(text) : null;
|
|
105
|
-
if (!res.ok) {
|
|
106
|
-
const envelope = parsed?.error ?? null;
|
|
107
|
-
throw new RonanaApiError(envelope?.code ?? "UNKNOWN", envelope?.message ?? "Upload failed.", res.status, envelope);
|
|
108
|
-
}
|
|
109
|
-
return parsed;
|
|
110
|
-
}
|
|
111
|
-
/** Upload a file larger than 4 MB: sign, PUT straight to storage, finalize.
|
|
112
|
-
*
|
|
113
|
-
* CAUTION: The PUT must send exactly the content type and the content length
|
|
114
|
-
* that were signed. Any other value fails the storage signature check. */
|
|
115
|
-
async uploadPresigned(bytes, filename, contentType) {
|
|
116
|
-
const signed = await this.request("POST", "/api/v1/refs/upload-url", { filename, contentType, size: bytes.byteLength });
|
|
117
|
-
const put = await fetch(signed.uploadUrl, {
|
|
118
|
-
method: "PUT",
|
|
119
|
-
headers: { "content-type": signed.contentType, "content-length": String(bytes.byteLength) },
|
|
120
|
-
body: bytes,
|
|
121
|
-
});
|
|
122
|
-
if (!put.ok)
|
|
123
|
-
throw new Error(`The upload to storage failed with status ${put.status}.`);
|
|
124
|
-
return this.request("POST", "/api/v1/refs", { tmpKey: signed.tmpKey, filename });
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
/** The most bytes an inline upload may carry. Above this, use the signed flow. */
|
|
128
|
-
export const INLINE_UPLOAD_MAX_BYTES = 4 * 1024 * 1024;
|
package/dist/schemas.d.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
/** MCP request schemas mirror the public HTTP schemas.
|
|
3
|
-
* Contract tests compare their fields and boundaries with those schemas. */
|
|
4
|
-
export declare const estimateCostInputSchema: {
|
|
5
|
-
readonly modelId: z.ZodString;
|
|
6
|
-
readonly resolution: z.ZodOptional<z.ZodString>;
|
|
7
|
-
readonly aspectRatio: z.ZodOptional<z.ZodString>;
|
|
8
|
-
readonly durationSeconds: z.ZodOptional<z.ZodNumber>;
|
|
9
|
-
readonly quality: z.ZodOptional<z.ZodEnum<["low", "medium", "high"]>>;
|
|
10
|
-
readonly hasVideoRefs: z.ZodOptional<z.ZodBoolean>;
|
|
11
|
-
readonly generateAudio: z.ZodOptional<z.ZodBoolean>;
|
|
12
|
-
readonly params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodNumber, z.ZodString, z.ZodBoolean]>>>;
|
|
13
|
-
};
|
|
14
|
-
export declare const generateInputSchema: {
|
|
15
|
-
readonly model: z.ZodString;
|
|
16
|
-
readonly prompt: z.ZodOptional<z.ZodString>;
|
|
17
|
-
readonly negativePrompt: z.ZodOptional<z.ZodString>;
|
|
18
|
-
readonly aspectRatio: z.ZodOptional<z.ZodString>;
|
|
19
|
-
readonly resolution: z.ZodOptional<z.ZodString>;
|
|
20
|
-
readonly attachments: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
21
|
-
source: z.ZodEnum<["ref", "video_generation", "image_generation", "media_asset"]>;
|
|
22
|
-
id: z.ZodString;
|
|
23
|
-
}, "strip", z.ZodTypeAny, {
|
|
24
|
-
id: string;
|
|
25
|
-
source: "ref" | "video_generation" | "image_generation" | "media_asset";
|
|
26
|
-
}, {
|
|
27
|
-
id: string;
|
|
28
|
-
source: "ref" | "video_generation" | "image_generation" | "media_asset";
|
|
29
|
-
}>, "many">>;
|
|
30
|
-
readonly seed: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
31
|
-
readonly params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodNumber, z.ZodString, z.ZodBoolean]>>>;
|
|
32
|
-
readonly idempotencyKey: z.ZodOptional<z.ZodString>;
|
|
33
|
-
readonly wait: z.ZodOptional<z.ZodBoolean>;
|
|
34
|
-
};
|
|
35
|
-
export declare const videoGenerationInputSchema: {
|
|
36
|
-
readonly slotBindings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
37
|
-
readonly keyframes: z.ZodOptional<z.ZodEffects<z.ZodRecord<z.ZodString, z.ZodNumber>, Record<string, number>, Record<string, number>>>;
|
|
38
|
-
readonly duration: z.ZodOptional<z.ZodNumber>;
|
|
39
|
-
readonly generateAudio: z.ZodOptional<z.ZodBoolean>;
|
|
40
|
-
readonly movementAmplitude: z.ZodOptional<z.ZodEnum<["auto", "small", "medium", "large"]>>;
|
|
41
|
-
readonly shotType: z.ZodOptional<z.ZodEnum<["intelligence", "customize"]>>;
|
|
42
|
-
readonly multiPrompt: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
43
|
-
prompt: z.ZodString;
|
|
44
|
-
duration: z.ZodNumber;
|
|
45
|
-
shotSize: z.ZodOptional<z.ZodEnum<["auto", "wide", "medium", "closeup", "macro", "pov"]>>;
|
|
46
|
-
cameraMove: z.ZodOptional<z.ZodEnum<["auto", "static", "pan", "tracking", "dollyIn", "dollyOut", "zoom"]>>;
|
|
47
|
-
}, "strip", z.ZodTypeAny, {
|
|
48
|
-
prompt: string;
|
|
49
|
-
duration: number;
|
|
50
|
-
shotSize?: "medium" | "auto" | "wide" | "closeup" | "macro" | "pov" | undefined;
|
|
51
|
-
cameraMove?: "auto" | "static" | "pan" | "tracking" | "dollyIn" | "dollyOut" | "zoom" | undefined;
|
|
52
|
-
}, {
|
|
53
|
-
prompt: string;
|
|
54
|
-
duration: number;
|
|
55
|
-
shotSize?: "medium" | "auto" | "wide" | "closeup" | "macro" | "pov" | undefined;
|
|
56
|
-
cameraMove?: "auto" | "static" | "pan" | "tracking" | "dollyIn" | "dollyOut" | "zoom" | undefined;
|
|
57
|
-
}>, "many">>;
|
|
58
|
-
readonly model: z.ZodString;
|
|
59
|
-
readonly prompt: z.ZodOptional<z.ZodString>;
|
|
60
|
-
readonly negativePrompt: z.ZodOptional<z.ZodString>;
|
|
61
|
-
readonly aspectRatio: z.ZodOptional<z.ZodString>;
|
|
62
|
-
readonly resolution: z.ZodOptional<z.ZodString>;
|
|
63
|
-
readonly attachments: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
64
|
-
source: z.ZodEnum<["ref", "video_generation", "image_generation", "media_asset"]>;
|
|
65
|
-
id: z.ZodString;
|
|
66
|
-
}, "strip", z.ZodTypeAny, {
|
|
67
|
-
id: string;
|
|
68
|
-
source: "ref" | "video_generation" | "image_generation" | "media_asset";
|
|
69
|
-
}, {
|
|
70
|
-
id: string;
|
|
71
|
-
source: "ref" | "video_generation" | "image_generation" | "media_asset";
|
|
72
|
-
}>, "many">>;
|
|
73
|
-
readonly seed: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
74
|
-
readonly params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodNumber, z.ZodString, z.ZodBoolean]>>>;
|
|
75
|
-
readonly idempotencyKey: z.ZodOptional<z.ZodString>;
|
|
76
|
-
readonly wait: z.ZodOptional<z.ZodBoolean>;
|
|
77
|
-
};
|
|
78
|
-
export declare const imageGenerationInputSchema: {
|
|
79
|
-
readonly quality: z.ZodOptional<z.ZodEnum<["low", "medium", "high"]>>;
|
|
80
|
-
readonly webSearch: z.ZodOptional<z.ZodBoolean>;
|
|
81
|
-
readonly maskDataUrl: z.ZodOptional<z.ZodString>;
|
|
82
|
-
readonly model: z.ZodString;
|
|
83
|
-
readonly prompt: z.ZodOptional<z.ZodString>;
|
|
84
|
-
readonly negativePrompt: z.ZodOptional<z.ZodString>;
|
|
85
|
-
readonly aspectRatio: z.ZodOptional<z.ZodString>;
|
|
86
|
-
readonly resolution: z.ZodOptional<z.ZodString>;
|
|
87
|
-
readonly attachments: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
88
|
-
source: z.ZodEnum<["ref", "video_generation", "image_generation", "media_asset"]>;
|
|
89
|
-
id: z.ZodString;
|
|
90
|
-
}, "strip", z.ZodTypeAny, {
|
|
91
|
-
id: string;
|
|
92
|
-
source: "ref" | "video_generation" | "image_generation" | "media_asset";
|
|
93
|
-
}, {
|
|
94
|
-
id: string;
|
|
95
|
-
source: "ref" | "video_generation" | "image_generation" | "media_asset";
|
|
96
|
-
}>, "many">>;
|
|
97
|
-
readonly seed: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
98
|
-
readonly params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodNumber, z.ZodString, z.ZodBoolean]>>>;
|
|
99
|
-
readonly idempotencyKey: z.ZodOptional<z.ZodString>;
|
|
100
|
-
readonly wait: z.ZodOptional<z.ZodBoolean>;
|
|
101
|
-
};
|
package/dist/schemas.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
const MAX_ATTACHMENTS_PER_SUBMIT = 50;
|
|
3
|
-
/** MCP request schemas mirror the public HTTP schemas.
|
|
4
|
-
* Contract tests compare their fields and boundaries with those schemas. */
|
|
5
|
-
export const estimateCostInputSchema = {
|
|
6
|
-
modelId: z.string().min(1).max(300).describe("A model id from list_models."),
|
|
7
|
-
resolution: z.string().max(40).optional(),
|
|
8
|
-
aspectRatio: z.string().max(20).optional(),
|
|
9
|
-
durationSeconds: z.number().min(-1).max(600).optional(),
|
|
10
|
-
quality: z.enum(["low", "medium", "high"]).optional(),
|
|
11
|
-
hasVideoRefs: z.boolean().optional(),
|
|
12
|
-
generateAudio: z.boolean().optional(),
|
|
13
|
-
params: z.record(z.string().max(64), z.union([
|
|
14
|
-
z.number(), z.string().max(64), z.boolean(),
|
|
15
|
-
])).optional(),
|
|
16
|
-
};
|
|
17
|
-
const attachment = z.object({
|
|
18
|
-
source: z.enum(["ref", "video_generation", "image_generation", "media_asset"]),
|
|
19
|
-
id: z.string().min(1).max(128),
|
|
20
|
-
});
|
|
21
|
-
export const generateInputSchema = {
|
|
22
|
-
model: z.string().min(1).max(120).describe("A model id from list_models."),
|
|
23
|
-
prompt: z.string().min(1).max(25_000).optional(),
|
|
24
|
-
negativePrompt: z.string().max(1000).optional(),
|
|
25
|
-
aspectRatio: z.string().min(1).max(16).optional(),
|
|
26
|
-
resolution: z.string().min(1).max(16).optional(),
|
|
27
|
-
attachments: z.array(attachment).max(MAX_ATTACHMENTS_PER_SUBMIT).optional(),
|
|
28
|
-
seed: z.number().int().min(0).max(4294967295).nullable().optional(),
|
|
29
|
-
params: z.record(z.string().max(64), z.union([
|
|
30
|
-
z.number(), z.string().max(64), z.boolean(),
|
|
31
|
-
])).optional(),
|
|
32
|
-
idempotencyKey: z.string().max(64).optional(),
|
|
33
|
-
wait: z.boolean().optional(),
|
|
34
|
-
};
|
|
35
|
-
export const videoGenerationInputSchema = {
|
|
36
|
-
...generateInputSchema,
|
|
37
|
-
slotBindings: z.record(z.string().max(32), z.string().max(128)).optional(),
|
|
38
|
-
keyframes: z.record(z.string().max(128), z.number().min(0).max(3600)).refine((value) => Object.keys(value).length <= 20, "Maximum 20 keyframes.").optional(),
|
|
39
|
-
duration: z.number().int().min(-1).max(60).optional(),
|
|
40
|
-
generateAudio: z.boolean().optional(),
|
|
41
|
-
movementAmplitude: z.enum(["auto", "small", "medium", "large"]).optional(),
|
|
42
|
-
shotType: z.enum(["intelligence", "customize"]).optional(),
|
|
43
|
-
multiPrompt: z.array(z.object({
|
|
44
|
-
prompt: z.string().min(1).max(2500),
|
|
45
|
-
duration: z.number().int().min(1).max(15),
|
|
46
|
-
shotSize: z.enum(["auto", "wide", "medium", "closeup", "macro", "pov"]).optional(),
|
|
47
|
-
cameraMove: z.enum(["auto", "static", "pan", "tracking", "dollyIn", "dollyOut", "zoom"]).optional(),
|
|
48
|
-
})).max(6).optional(),
|
|
49
|
-
};
|
|
50
|
-
export const imageGenerationInputSchema = {
|
|
51
|
-
...generateInputSchema,
|
|
52
|
-
quality: z.enum(["low", "medium", "high"]).optional(),
|
|
53
|
-
webSearch: z.boolean().optional(),
|
|
54
|
-
maskDataUrl: z.string().max(7_000_000).optional(),
|
|
55
|
-
};
|
package/dist/types.d.ts
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
export type RefKind = "image" | "video" | "audio";
|
|
2
|
-
export interface ApiSlotSpec {
|
|
3
|
-
id: string;
|
|
4
|
-
kind: RefKind;
|
|
5
|
-
required: boolean;
|
|
6
|
-
}
|
|
7
|
-
export interface ApiRefSpec {
|
|
8
|
-
kinds: readonly RefKind[];
|
|
9
|
-
max: {
|
|
10
|
-
images?: number;
|
|
11
|
-
videos?: number;
|
|
12
|
-
audio?: number;
|
|
13
|
-
total?: number;
|
|
14
|
-
videosTotalSec?: number;
|
|
15
|
-
audioTotalSec?: number;
|
|
16
|
-
videoMaxBytes?: number;
|
|
17
|
-
audioMaxBytes?: number;
|
|
18
|
-
videoMinSec?: number;
|
|
19
|
-
videoMinWidthPx?: number;
|
|
20
|
-
videoMaxWidthPx?: number;
|
|
21
|
-
imageMinPx?: number;
|
|
22
|
-
imageMaxPx?: number;
|
|
23
|
-
imageMaxBytes?: number;
|
|
24
|
-
imageMaxPixels?: number;
|
|
25
|
-
imageMinAspect?: number;
|
|
26
|
-
imageMaxAspect?: number;
|
|
27
|
-
videoMaxSec?: number;
|
|
28
|
-
};
|
|
29
|
-
inline: boolean;
|
|
30
|
-
slots: readonly ApiSlotSpec[];
|
|
31
|
-
attachmentGroups: {
|
|
32
|
-
tokenPrefix?: string;
|
|
33
|
-
maxGroups: number;
|
|
34
|
-
kinds: Array<"image" | "video" | "audio">;
|
|
35
|
-
minPerGroup: number;
|
|
36
|
-
maxPerGroup: number;
|
|
37
|
-
primaryFirst?: boolean;
|
|
38
|
-
} | null;
|
|
39
|
-
}
|
|
40
|
-
export type ApiParamSpec = {
|
|
41
|
-
id: string;
|
|
42
|
-
kind: "number";
|
|
43
|
-
min: number;
|
|
44
|
-
max: number;
|
|
45
|
-
step: number;
|
|
46
|
-
} | {
|
|
47
|
-
id: string;
|
|
48
|
-
kind: "select";
|
|
49
|
-
options: readonly string[];
|
|
50
|
-
} | {
|
|
51
|
-
id: string;
|
|
52
|
-
kind: "toggle";
|
|
53
|
-
};
|
|
54
|
-
export interface ApiInputConditionedRule {
|
|
55
|
-
mode: "t2v" | "i2v" | "r2v";
|
|
56
|
-
aspect?: string[];
|
|
57
|
-
resolution?: string[];
|
|
58
|
-
duration?: number[];
|
|
59
|
-
when: readonly {
|
|
60
|
-
imageRefs: "0" | "1" | "2+" | "any";
|
|
61
|
-
audioRefs: "0" | "1+" | "any";
|
|
62
|
-
}[];
|
|
63
|
-
}
|
|
64
|
-
export interface ApiModelSpec {
|
|
65
|
-
id: string;
|
|
66
|
-
kind: "video" | "image";
|
|
67
|
-
name: string;
|
|
68
|
-
prompt: {
|
|
69
|
-
required: boolean;
|
|
70
|
-
maxChars?: number;
|
|
71
|
-
};
|
|
72
|
-
aspect: readonly string[];
|
|
73
|
-
aspectWithImage: readonly string[];
|
|
74
|
-
resolution: readonly string[];
|
|
75
|
-
duration: readonly number[];
|
|
76
|
-
quality: readonly ("low" | "medium" | "high")[];
|
|
77
|
-
dimensionsFromReference: boolean;
|
|
78
|
-
refs: ApiRefSpec;
|
|
79
|
-
params: readonly ApiParamSpec[];
|
|
80
|
-
variants: readonly ApiInputConditionedRule[];
|
|
81
|
-
multiShot: boolean;
|
|
82
|
-
seed: boolean;
|
|
83
|
-
negativePrompt: boolean;
|
|
84
|
-
generateAudio: boolean;
|
|
85
|
-
movementAmplitude: boolean;
|
|
86
|
-
webSearch: boolean;
|
|
87
|
-
mask: {
|
|
88
|
-
format: "png-data-url";
|
|
89
|
-
dimensions: "match-first-image";
|
|
90
|
-
polarity: "alpha-punch" | "white-on-black";
|
|
91
|
-
} | null;
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* What the server actually SENDS for a model.
|
|
95
|
-
*
|
|
96
|
-
* `ApiModelSpec` above is the total shape. The wire is smaller, twice over: it
|
|
97
|
-
* drops every field carrying no information (`false`, `null`, an empty list),
|
|
98
|
-
* and it drops the fields only the studio uses (`category`, `preset`, and the
|
|
99
|
-
* `group`/`label` split, which arrives as one `name`). A capability the model
|
|
100
|
-
* does not have is ABSENT rather than sent as a "no", so read presence as
|
|
101
|
-
* support:
|
|
102
|
-
*
|
|
103
|
-
* if (model.seed) { ... } // correct, whether absent or true
|
|
104
|
-
* for (const s of model.refs?.slots ?? []) { ... }
|
|
105
|
-
*
|
|
106
|
-
* Derived from `ApiModelSpec`, never hand-written, so a new field on the server
|
|
107
|
-
* reaches this type with no second edit here.
|
|
108
|
-
*/
|
|
109
|
-
type Loosen<T> = {
|
|
110
|
-
[K in keyof T]?: T[K];
|
|
111
|
-
};
|
|
112
|
-
export type InternalModelField = never;
|
|
113
|
-
export type ApiPublicModel = Loosen<Omit<ApiModelSpec, "refs" | "prompt" | InternalModelField>> & {
|
|
114
|
-
/** The model's display name, in one piece. */
|
|
115
|
-
name?: string;
|
|
116
|
-
refs?: Loosen<ApiRefSpec>;
|
|
117
|
-
prompt?: Loosen<ApiModelSpec["prompt"]>;
|
|
118
|
-
};
|
|
119
|
-
/** The error envelope every v1 endpoint returns on a failure. */
|
|
120
|
-
export interface ApiErrorBody {
|
|
121
|
-
error: {
|
|
122
|
-
code: "AUTH_REQUIRED" | "AUTH_INVALID" | "AUTH_EXPIRED" | "SCOPE_INSUFFICIENT" | "ACCOUNT_SUSPENDED" | "ACCOUNT_PENDING_DELETION" | "EMAIL_UNVERIFIED" | "PLAN_UPGRADE_REQUIRED" | "CREDITS_INSUFFICIENT" | "SPEND_LOCKED" | "DAILY_CREDIT_CAP" | "KEY_DAILY_CREDIT_CAP" | "DAILY_GENERATION_CAP" | "STORAGE_FULL" | "RATE_LIMITED" | "CONCURRENCY_LIMIT" | "INVALID_REQUEST" | "MODEL_UNKNOWN" | "MODEL_DISABLED" | "NOT_FOUND" | "PROMPT_TOO_LONG" | "UNSUPPORTED_OPTION" | "LAUNCH_CLOSED" | "SERVICE_UNAVAILABLE" | "INTERNAL";
|
|
123
|
-
message: string;
|
|
124
|
-
requestId: string;
|
|
125
|
-
retryAfterMs?: number;
|
|
126
|
-
resetAt?: string;
|
|
127
|
-
field?: string;
|
|
128
|
-
billing?: {
|
|
129
|
-
balance?: number;
|
|
130
|
-
needed?: number;
|
|
131
|
-
currentTier?: string;
|
|
132
|
-
requiredTier?: string;
|
|
133
|
-
nextTier?: {
|
|
134
|
-
id: string;
|
|
135
|
-
creditsMonthly: number;
|
|
136
|
-
} | null;
|
|
137
|
-
purchasesEnabled?: boolean;
|
|
138
|
-
atMaxTier?: boolean;
|
|
139
|
-
action: "buy_credits" | "upgrade_plan" | "either" | "verify_email" | "wait" | "contact_support";
|
|
140
|
-
url: string;
|
|
141
|
-
};
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
export interface GenerationState {
|
|
145
|
-
id: string;
|
|
146
|
-
kind: "video" | "image";
|
|
147
|
-
status: "queued" | "processing" | "completed" | "failed" | "canceled";
|
|
148
|
-
model: string;
|
|
149
|
-
prompt: string;
|
|
150
|
-
aspectRatio: string;
|
|
151
|
-
resolution: string;
|
|
152
|
-
durationS?: number;
|
|
153
|
-
quality?: "low" | "medium" | "high" | null;
|
|
154
|
-
source: "api";
|
|
155
|
-
output: {
|
|
156
|
-
url: string | null;
|
|
157
|
-
expiresAt: string | null;
|
|
158
|
-
bytes: number | null;
|
|
159
|
-
width: number | null;
|
|
160
|
-
height: number | null;
|
|
161
|
-
durationS?: number | null;
|
|
162
|
-
retentionExpiresAt: string | null;
|
|
163
|
-
};
|
|
164
|
-
failure: {
|
|
165
|
-
reason: string;
|
|
166
|
-
title: string;
|
|
167
|
-
detail: string;
|
|
168
|
-
detailKey?: string;
|
|
169
|
-
} | null;
|
|
170
|
-
createdAt: string;
|
|
171
|
-
completedAt: string | null;
|
|
172
|
-
}
|
|
173
|
-
export {};
|
package/dist/types.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
// The public shapes this package reads, declared with NO runtime import.
|
|
2
|
-
//
|
|
3
|
-
// They are a copy of the types the server publishes at `GET /api/v1/models`
|
|
4
|
-
// (lib/api/modelSchema.ts). The copy is deliberate: this package must run from
|
|
5
|
-
// npm with nothing but `RONANA_API_KEY` and `RONANA_BASE_URL`, so it can hold no
|
|
6
|
-
// path into the application repository.
|
|
7
|
-
//
|
|
8
|
-
// tests/mcpPublicTypes.test.ts asserts the two definitions stay compatible, in
|
|
9
|
-
// both directions. If somebody adds a field to `ApiModelSpec` and forgets this
|
|
10
|
-
// file, that test fails.
|
|
11
|
-
export {};
|