@ronana/mcp 0.1.1 → 0.3.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/LICENSE +21 -0
- package/README.md +29 -6
- package/dist/cli.js +91 -11
- package/dist/input.js +4 -2
- package/dist/oauth.js +11 -1
- 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
|
|
|
@@ -38,6 +37,8 @@ ronana generate cost google/nano-banana-2-lite/edit --prompt "make it dusk" --
|
|
|
38
37
|
ronana generate create google/nano-banana-2-lite/edit --prompt "make it dusk" --image ./photo.png --out ./renders
|
|
39
38
|
ronana generate get <id> | wait <id> [--out dir]
|
|
40
39
|
ronana generate list
|
|
40
|
+
ronana upscale <id | file | url> --resolution 4k --out ./renders
|
|
41
|
+
ronana folder list | add <folder> <id…>
|
|
41
42
|
```
|
|
42
43
|
|
|
43
44
|
Every field from `model get` is a flag (`--aspect_ratio 16:9`, `--seed 7`).
|
|
@@ -81,9 +82,27 @@ add it to the app's MCP settings:
|
|
|
81
82
|
}
|
|
82
83
|
```
|
|
83
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
|
+
|
|
84
102
|
The local server adds one tool, `upload_local_reference`, which uploads a file
|
|
85
103
|
from inside `RONANA_MCP_UPLOAD_ROOT`. Set it to the folder that holds your
|
|
86
|
-
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
|
|
87
106
|
straight to storage, and Ronana checks each file before it adds it to your
|
|
88
107
|
library.
|
|
89
108
|
|
|
@@ -95,18 +114,22 @@ library.
|
|
|
95
114
|
| `get_model` | Get one model's contract: its input schema. |
|
|
96
115
|
| `estimate_cost` | Estimate the credit cost of a generation. Takes the same `model` and `input` as `generate`. |
|
|
97
116
|
| `generate` | Start an image or video generation. |
|
|
98
|
-
| `get_generation` | Get one generation and its download link. |
|
|
117
|
+
| `get_generation` | Get one generation and its download link. Pass `wait_seconds` (up to 45) to wait for the result in one call. |
|
|
118
|
+
| `upscale` | Upscale a video to a higher resolution. |
|
|
99
119
|
| `list_generations` | List recent generations. |
|
|
100
120
|
| `list_references` | List library references. |
|
|
101
121
|
| `upload_reference` | Add a file to the library from a URL (up to 25 MB) or from base64 (under 3 MB). |
|
|
102
122
|
| `create_upload` | Get a private upload link for a larger local file (up to 200 MB). |
|
|
103
123
|
| `finish_upload` | Add a file uploaded with `create_upload` to the library. |
|
|
124
|
+
| `list_folders` | List the folders in the library. |
|
|
125
|
+
| `add_to_folder` | File library items into a folder. |
|
|
104
126
|
| `get_account` | Get the plan and credit balance. |
|
|
105
127
|
|
|
106
128
|
A generate call takes a `model` and an `input`, exactly as `get_model` shows it
|
|
107
129
|
in `input_schema`. A media field takes a public https URL or a Ronana id
|
|
108
|
-
(`ref_…`, `img_…`, `gen_…`). Generation tools return an id immediately.
|
|
109
|
-
`get_generation` until the status is `completed` or
|
|
130
|
+
(`ref_…`, `img_…`, `gen_…`). Generation tools return an id immediately. Call
|
|
131
|
+
`get_generation` with `wait_seconds` until the status is `completed` or
|
|
132
|
+
`failed`. Apps that support MCP Apps show the image or video right in the chat.
|
|
110
133
|
|
|
111
134
|
```json
|
|
112
135
|
{ "model": "google/nano-banana-2/edit", "input": { "prompt": "a lantern shop at dusk", "images": ["https://example.com/ref.png"] } }
|
package/dist/cli.js
CHANGED
|
@@ -12,15 +12,17 @@
|
|
|
12
12
|
// ronana generate cost <model> [same flags as create]
|
|
13
13
|
// ronana generate get | wait <id> [--out dir]
|
|
14
14
|
// ronana generate list [--limit n]
|
|
15
|
+
// ronana upscale <id | file | https-url> [--resolution r] [--model m] [--wait] [--out dir]
|
|
16
|
+
// ronana folder list | add <folder> <id…>
|
|
15
17
|
import { randomUUID } from "node:crypto";
|
|
16
|
-
import { createWriteStream } from "node:fs";
|
|
18
|
+
import { createWriteStream, existsSync, statSync } from "node:fs";
|
|
17
19
|
import { mkdir } from "node:fs/promises";
|
|
18
20
|
import { basename, extname, join, resolve } from "node:path";
|
|
19
21
|
import { Readable } from "node:stream";
|
|
20
22
|
import { pipeline } from "node:stream/promises";
|
|
21
23
|
import { login, logout, readCredentials } from "./oauth.js";
|
|
22
24
|
import { callTool, remoteClient, ToolError, uploadFile } from "./remote.js";
|
|
23
|
-
import { buildInput, flag, has, parseArgs, replaceLocalMedia, typeOf } from "./input.js";
|
|
25
|
+
import { buildInput, flag, has, isRemoteValue, parseArgs, replaceLocalMedia, typeOf } from "./input.js";
|
|
24
26
|
const HELP = `Usage: ronana <command>
|
|
25
27
|
|
|
26
28
|
auth login Sign in with your browser (--base-url, --no-browser)
|
|
@@ -35,6 +37,9 @@ const HELP = `Usage: ronana <command>
|
|
|
35
37
|
generate get <id> Status and download links
|
|
36
38
|
generate wait <id> Wait for a generation to finish
|
|
37
39
|
generate list Recent generations (--limit n)
|
|
40
|
+
upscale <id|file|url> Upscale a video (costs credits; --resolution, --model)
|
|
41
|
+
folder list Your library folders
|
|
42
|
+
folder add <folder> <id…> File items into a folder (made if missing)
|
|
38
43
|
|
|
39
44
|
Generate flags:
|
|
40
45
|
--<field> <value> Any field from \`model get\`. Repeat for lists.
|
|
@@ -65,6 +70,8 @@ function describeField(name, schema, required) {
|
|
|
65
70
|
parts.push(`default ${JSON.stringify(schema.default)}`);
|
|
66
71
|
return ` --${name.padEnd(22)} ${parts.join(" ")}${required ? " (required)" : ""}`;
|
|
67
72
|
}
|
|
73
|
+
/** Model ids are lowercase paths; forgive the casing and a trailing slash. */
|
|
74
|
+
const modelId = (raw) => raw.trim().replace(/\/+$/, "").toLowerCase();
|
|
68
75
|
const TERMINAL = new Set(["completed", "failed", "canceled"]);
|
|
69
76
|
function seconds(args, name, fallback) {
|
|
70
77
|
const raw = flag(args, name);
|
|
@@ -80,8 +87,12 @@ async function waitFor(remote, id, args) {
|
|
|
80
87
|
let last = "";
|
|
81
88
|
for (;;) {
|
|
82
89
|
let gen;
|
|
90
|
+
const asked = Date.now();
|
|
83
91
|
try {
|
|
84
|
-
|
|
92
|
+
// The server holds the call until the job settles (up to 25s), so this
|
|
93
|
+
// loop makes one request per 25s rather than one every few seconds.
|
|
94
|
+
const wait_seconds = Math.max(0, Math.min(25, Math.floor((deadline - asked) / 1000)));
|
|
95
|
+
gen = await callTool(remote, "get_generation", { id, wait_seconds });
|
|
85
96
|
}
|
|
86
97
|
catch (error) {
|
|
87
98
|
// A server answer (not found, no access) is final; anything else is the
|
|
@@ -99,7 +110,11 @@ ${id} is still running. Resume with \`ronana generate wait ${id}\`.`);
|
|
|
99
110
|
return gen;
|
|
100
111
|
if (Date.now() > deadline)
|
|
101
112
|
throw new Error(`Still ${gen.status} after ${timeoutMs / 1000}s. Check later with \`ronana generate wait ${id}\`.`);
|
|
102
|
-
|
|
113
|
+
// A server that answered at once (an older one, or a short wait) still gets
|
|
114
|
+
// the local pause, so the loop never spins.
|
|
115
|
+
const left = intervalMs - (Date.now() - asked);
|
|
116
|
+
if (left > 0)
|
|
117
|
+
await new Promise((r) => setTimeout(r, left));
|
|
103
118
|
}
|
|
104
119
|
}
|
|
105
120
|
const EXT_BY_TYPE = {
|
|
@@ -107,6 +122,8 @@ const EXT_BY_TYPE = {
|
|
|
107
122
|
"video/mp4": ".mp4", "video/webm": ".webm", "video/quicktime": ".mov",
|
|
108
123
|
};
|
|
109
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.`);
|
|
110
127
|
await mkdir(dir, { recursive: true });
|
|
111
128
|
const saved = [];
|
|
112
129
|
for (const output of gen.outputs) {
|
|
@@ -160,6 +177,8 @@ async function generateCommand(sub, args) {
|
|
|
160
177
|
const [target] = args.positional;
|
|
161
178
|
if (sub === "list") {
|
|
162
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.");
|
|
163
182
|
const rows = await withRemote((r) => callTool(r, "list_generations", { limit }));
|
|
164
183
|
return emit(rows, () => rows.forEach((g) => print(`${g.id.padEnd(28)} ${g.status.padEnd(11)} ${g.created_at}`)));
|
|
165
184
|
}
|
|
@@ -174,7 +193,7 @@ async function generateCommand(sub, args) {
|
|
|
174
193
|
if (sub !== "create" && sub !== "cost")
|
|
175
194
|
throw new Error("Use generate create | cost | get | wait | list.");
|
|
176
195
|
await withRemote(async (remote) => {
|
|
177
|
-
const contract = await callTool(remote, "get_model", { id: target });
|
|
196
|
+
const contract = await callTool(remote, "get_model", { id: modelId(target) });
|
|
178
197
|
const input = await buildInput(contract, args);
|
|
179
198
|
await replaceLocalMedia(contract, input, async (path) => {
|
|
180
199
|
note(`Uploading ${basename(path)}…`);
|
|
@@ -199,17 +218,23 @@ async function generateCommand(sub, args) {
|
|
|
199
218
|
async function modelCommand(sub, args) {
|
|
200
219
|
if (sub === "list" || sub === undefined) {
|
|
201
220
|
const kind = flag(args, "kind");
|
|
221
|
+
if (kind !== undefined && kind !== "image" && kind !== "video")
|
|
222
|
+
throw new Error("--kind must be image or video.");
|
|
202
223
|
const filter = args.positional[0]?.toLowerCase();
|
|
203
224
|
const models = (await withRemote((r) => callTool(r, "list_models", kind ? { kind } : {})))
|
|
204
225
|
.filter((m) => !filter || m.id.toLowerCase().includes(filter) || m.name.toLowerCase().includes(filter));
|
|
205
|
-
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
|
+
});
|
|
206
231
|
}
|
|
207
232
|
if (sub !== "get")
|
|
208
233
|
throw new Error("Use model list | get.");
|
|
209
234
|
const id = args.positional[0];
|
|
210
235
|
if (!id)
|
|
211
236
|
throw new Error("Usage: ronana model get <model>");
|
|
212
|
-
const contract = await withRemote((r) => callTool(r, "get_model", { id }));
|
|
237
|
+
const contract = await withRemote((r) => callTool(r, "get_model", { id: modelId(id) }));
|
|
213
238
|
emit(contract, () => {
|
|
214
239
|
print(`${contract.name} (${contract.kind})`);
|
|
215
240
|
print(contract.id);
|
|
@@ -225,10 +250,54 @@ async function modelCommand(sub, args) {
|
|
|
225
250
|
print(`Priced by: ${contract.priced_fields.join(", ")}`);
|
|
226
251
|
});
|
|
227
252
|
}
|
|
253
|
+
async function upscaleCommand(args) {
|
|
254
|
+
const [source] = args.positional;
|
|
255
|
+
if (!source)
|
|
256
|
+
throw new Error("Usage: ronana upscale <gen_…|ref_… id | file | https-url> [--resolution 4k] [--out dir]");
|
|
257
|
+
await withRemote(async (remote) => {
|
|
258
|
+
// A local file uploads first; an id or a URL goes as-is.
|
|
259
|
+
const resolved = isRemoteValue(source) ? source : (note(`Uploading ${basename(source)}…`), (await uploadFile(remote, resolve(source))).id);
|
|
260
|
+
const queued = await callTool(remote, "upscale", {
|
|
261
|
+
source: resolved,
|
|
262
|
+
...(flag(args, "model") ? { model: flag(args, "model") } : {}),
|
|
263
|
+
...(flag(args, "resolution") ? { resolution: flag(args, "resolution") } : {}),
|
|
264
|
+
idempotency_key: flag(args, "idempotency_key") ?? randomUUID(),
|
|
265
|
+
});
|
|
266
|
+
if (!has(args, "wait") && !has(args, "out")) {
|
|
267
|
+
return emit(queued, () => { print(queued.id); note(`Check it with \`ronana generate wait ${queued.id}\`.`); });
|
|
268
|
+
}
|
|
269
|
+
note(`Queued ${queued.id}.`);
|
|
270
|
+
await finish(await waitFor(remote, queued.id, args), args);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
async function folderCommand(sub, args) {
|
|
274
|
+
if (sub === "list" || sub === undefined) {
|
|
275
|
+
const folders = await withRemote((r) => callTool(r, "list_folders"));
|
|
276
|
+
return emit(folders, () => folders.forEach((f) => print(`${f.id.padEnd(28)} ${String(f.count).padStart(5)} ${f.name}`)));
|
|
277
|
+
}
|
|
278
|
+
if (sub !== "add")
|
|
279
|
+
throw new Error("Use folder list | add.");
|
|
280
|
+
const [folder, ...ids] = args.positional;
|
|
281
|
+
if (!folder?.trim() || ids.length === 0)
|
|
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.");
|
|
285
|
+
const filed = await withRemote((r) => callTool(r, "add_to_folder", { ids, folder, create: true }));
|
|
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;
|
|
293
|
+
}
|
|
228
294
|
async function main() {
|
|
229
|
-
const [command,
|
|
230
|
-
const takesSub = command === "auth" || command === "model" || command === "generate";
|
|
231
|
-
const args = parseArgs(
|
|
295
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
296
|
+
const takesSub = command === "auth" || command === "model" || command === "generate" || command === "folder";
|
|
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;
|
|
232
301
|
JSON_OUT = has(args, "json");
|
|
233
302
|
if (!command || ["help", "--help", "-h"].includes(command) || has(args, "help"))
|
|
234
303
|
return void process.stdout.write(HELP);
|
|
@@ -262,18 +331,29 @@ async function main() {
|
|
|
262
331
|
const source = args.positional[0];
|
|
263
332
|
if (!source)
|
|
264
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.");
|
|
265
336
|
const ref = await withRemote((r) => /^https:\/\//i.test(source)
|
|
266
337
|
? callTool(r, "upload_reference", { url: source, filename: basename(new URL(source).pathname) || "upload" })
|
|
267
338
|
: uploadFile(r, resolve(source)));
|
|
268
339
|
return emit(ref, () => print(ref.id));
|
|
269
340
|
}
|
|
270
341
|
case "generate": return generateCommand(sub, args);
|
|
342
|
+
case "upscale": return upscaleCommand(args);
|
|
343
|
+
case "folder":
|
|
344
|
+
case "folders": return folderCommand(command === "folders" ? "list" : sub, args);
|
|
271
345
|
default: throw new Error(`Unknown command "${command}". Run \`ronana --help\`.`);
|
|
272
346
|
}
|
|
273
347
|
}
|
|
274
348
|
main().catch((error) => {
|
|
275
349
|
if (error instanceof ToolError) {
|
|
276
|
-
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}`;
|
|
277
357
|
if (JSON_OUT)
|
|
278
358
|
process.stdout.write(`${JSON.stringify({ ok: false, error: error.error }, null, 2)}\n`);
|
|
279
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
|
@@ -64,6 +64,14 @@ async function tokenRequest(baseUrl, fields) {
|
|
|
64
64
|
}
|
|
65
65
|
export async function login(baseUrl = process.env.RONANA_BASE_URL ?? "https://ronana.ai", openInBrowser = true) {
|
|
66
66
|
baseUrl = baseUrl.replace(/\/$/, "");
|
|
67
|
+
const parsed = (() => { try {
|
|
68
|
+
return new URL(baseUrl);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
} })();
|
|
73
|
+
if (!parsed || !/^https?:$/.test(parsed.protocol))
|
|
74
|
+
throw new Error("--base-url must be a web address, like https://ronana.ai.");
|
|
67
75
|
const verifier = randomBytes(32).toString("base64url");
|
|
68
76
|
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
69
77
|
const state = randomBytes(24).toString("base64url");
|
|
@@ -113,8 +121,10 @@ export async function login(baseUrl = process.env.RONANA_BASE_URL ?? "https://ro
|
|
|
113
121
|
response_types: ["code"],
|
|
114
122
|
scope: SCOPES,
|
|
115
123
|
}),
|
|
124
|
+
}).catch(() => { throw new Error(`Could not reach ${baseUrl}. Check --base-url and your connection.`); });
|
|
125
|
+
const registered = await registration.json().catch(() => {
|
|
126
|
+
throw new Error(`No Ronana sign-in at ${baseUrl}. Check --base-url.`);
|
|
116
127
|
});
|
|
117
|
-
const registered = await registration.json();
|
|
118
128
|
if (!registration.ok || typeof registered.client_id !== "string")
|
|
119
129
|
throw new Error("OAuth client registration failed.");
|
|
120
130
|
const authorize = new URL(`${baseUrl}/api/auth/oauth2/authorize`);
|
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.0",
|
|
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 {};
|