@ronana/mcp 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js ADDED
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ import { realpath, stat } from "node:fs/promises";
3
+ import { isAbsolute, relative, resolve, sep } from "node:path";
4
+ import { fromJsonSchema, McpServer } from "@modelcontextprotocol/server";
5
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
6
+ import { z } from "zod4";
7
+ import { login, logout, readCredentials } from "./oauth.js";
8
+ import { remoteClient, ToolError, uploadFile } from "./remote.js";
9
+ const UPLOAD_ROOT = resolve(process.env.RONANA_MCP_UPLOAD_ROOT ?? process.cwd());
10
+ async function resolveInsideRoot(inputPath) {
11
+ const root = await realpath(UPLOAD_ROOT);
12
+ const candidate = await realpath(isAbsolute(inputPath) ? inputPath : resolve(root, inputPath));
13
+ const rel = relative(root, candidate);
14
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel))
15
+ throw new Error("The path is outside RONANA_MCP_UPLOAD_ROOT.");
16
+ return candidate;
17
+ }
18
+ async function serve() {
19
+ const remote = await remoteClient();
20
+ const { tools } = await remote.listTools();
21
+ serveStdio(() => {
22
+ const server = new McpServer({ name: "Ronana local bridge", version: "1.0.0" });
23
+ for (const tool of tools) {
24
+ server.registerTool(tool.name, {
25
+ title: tool.title,
26
+ description: tool.description,
27
+ inputSchema: fromJsonSchema(tool.inputSchema),
28
+ outputSchema: tool.outputSchema ? fromJsonSchema(tool.outputSchema) : undefined,
29
+ annotations: tool.annotations,
30
+ }, async (args) => remote.callTool({
31
+ name: tool.name,
32
+ arguments: args,
33
+ }));
34
+ }
35
+ server.registerTool("upload_local_reference", {
36
+ description: `Upload a local file below ${UPLOAD_ROOT} to the account library.`,
37
+ inputSchema: z.object({ path: z.string() }),
38
+ annotations: { readOnlyHint: false, destructiveHint: false },
39
+ }, async ({ path }) => {
40
+ try {
41
+ const safePath = await resolveInsideRoot(path);
42
+ if (!(await stat(safePath)).isFile())
43
+ throw new Error("The path is not a file.");
44
+ const body = { ok: true, data: await uploadFile(remote, safePath) };
45
+ return { content: [{ type: "text", text: JSON.stringify(body) }], structuredContent: body };
46
+ }
47
+ catch (error) {
48
+ // A server refusal keeps its own code; a local problem gets ours.
49
+ const body = { ok: false, error: error instanceof ToolError ? error.error
50
+ : { code: "LOCAL_UPLOAD_REJECTED", message: error instanceof Error ? error.message : "The upload failed." } };
51
+ return { isError: true, content: [{ type: "text", text: JSON.stringify(body) }], structuredContent: body };
52
+ }
53
+ });
54
+ return server;
55
+ });
56
+ }
57
+ async function main() {
58
+ const command = process.argv[2] ?? "serve";
59
+ if (command === "login") {
60
+ await login();
61
+ process.stdout.write("Connected.\n");
62
+ return;
63
+ }
64
+ if (command === "logout") {
65
+ await logout();
66
+ process.stdout.write("Disconnected.\n");
67
+ return;
68
+ }
69
+ if (command === "status") {
70
+ const credentials = readCredentials();
71
+ process.stdout.write(credentials ? `Connected to ${credentials.baseUrl}.\n` : "Not connected.\n");
72
+ return;
73
+ }
74
+ if (command === "serve") {
75
+ await serve();
76
+ return;
77
+ }
78
+ throw new Error("Use login, logout, status, or serve.");
79
+ }
80
+ main().catch((error) => {
81
+ process.stderr.write(`[ronana-mcp] ${error instanceof Error ? error.message : String(error)}\n`);
82
+ process.exitCode = 1;
83
+ });
@@ -0,0 +1,173 @@
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 ADDED
@@ -0,0 +1,11 @@
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 {};
package/package.json CHANGED
@@ -1,6 +1,28 @@
1
1
  {
2
2
  "name": "@ronana/mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Ronana command line and local MCP bridge.",
5
+ "license": "UNLICENSED",
6
+ "homepage": "https://ronana.ai",
7
+ "keywords": ["ronana", "mcp", "cli", "image generation", "video generation"],
8
+ "type": "module",
9
+ "bin": { "ronana": "./dist/cli.js", "ronana-mcp": "./dist/server.js" },
10
+ "files": ["dist", "README.md"],
11
+ "publishConfig": { "access": "public" },
12
+ "scripts": {
13
+ "build": "tsc -p tsconfig.json",
14
+ "prepublishOnly": "npm run build",
15
+ "start": "node --import tsx src/server.ts"
16
+ },
17
+ "dependencies": {
18
+ "@modelcontextprotocol/client": "2.0.0",
19
+ "@modelcontextprotocol/server": "2.0.0",
20
+ "@napi-rs/keyring": "1.3.0",
21
+ "zod4": "npm:zod@4.4.3"
22
+ },
23
+ "devDependencies": {
24
+ "typescript": "^5",
25
+ "tsx": "^4"
26
+ },
27
+ "engines": { "node": ">=20" }
28
+ }