@ronana/mcp 0.3.1 → 0.3.2
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/cli.js +16 -11
- package/dist/oauth.d.ts +2 -0
- package/dist/oauth.js +35 -12
- package/dist/remote.js +5 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -173,6 +173,14 @@ async function withRemote(fn) {
|
|
|
173
173
|
await remote.close().catch(() => null);
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
|
+
/** A queued job: print its id, or with --wait / --out follow it to the end. */
|
|
177
|
+
async function queuedThenFinish(remote, queued, args) {
|
|
178
|
+
if (!has(args, "wait") && !has(args, "out")) {
|
|
179
|
+
return emit(queued, () => { print(queued.id); note(`Check it with \`ronana generate wait ${queued.id}\`.`); });
|
|
180
|
+
}
|
|
181
|
+
note(`Queued ${queued.id}.`);
|
|
182
|
+
await finish(await waitFor(remote, queued.id, args), args);
|
|
183
|
+
}
|
|
176
184
|
async function generateCommand(sub, args) {
|
|
177
185
|
const [target] = args.positional;
|
|
178
186
|
if (sub === "list") {
|
|
@@ -208,11 +216,7 @@ async function generateCommand(sub, args) {
|
|
|
208
216
|
command run twice in ten minutes would return the first generation. */
|
|
209
217
|
const idempotency_key = flag(args, "idempotency_key") ?? randomUUID();
|
|
210
218
|
const queued = await callTool(remote, "generate", { model: contract.id, input, idempotency_key });
|
|
211
|
-
|
|
212
|
-
return emit(queued, () => { print(queued.id); note(`Check it with \`ronana generate wait ${queued.id}\`.`); });
|
|
213
|
-
}
|
|
214
|
-
note(`Queued ${queued.id}.`);
|
|
215
|
-
await finish(await waitFor(remote, queued.id, args), args);
|
|
219
|
+
await queuedThenFinish(remote, queued, args);
|
|
216
220
|
});
|
|
217
221
|
}
|
|
218
222
|
async function modelCommand(sub, args) {
|
|
@@ -263,11 +267,7 @@ async function upscaleCommand(args) {
|
|
|
263
267
|
...(flag(args, "resolution") ? { resolution: flag(args, "resolution") } : {}),
|
|
264
268
|
idempotency_key: flag(args, "idempotency_key") ?? randomUUID(),
|
|
265
269
|
});
|
|
266
|
-
|
|
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);
|
|
270
|
+
await queuedThenFinish(remote, queued, args);
|
|
271
271
|
});
|
|
272
272
|
}
|
|
273
273
|
async function folderCommand(sub, args) {
|
|
@@ -317,7 +317,12 @@ async function main() {
|
|
|
317
317
|
}
|
|
318
318
|
if (action === "status") {
|
|
319
319
|
const credentials = readCredentials();
|
|
320
|
-
|
|
320
|
+
if (!credentials)
|
|
321
|
+
return emit({ signed_in: false, base_url: null }, () => print("Not signed in."));
|
|
322
|
+
// Ask the server: a sign-in stored here may have been disconnected on
|
|
323
|
+
// the website or have expired, and only the server knows.
|
|
324
|
+
const problem = await withRemote((r) => callTool(r, "get_account")).then(() => null, (error) => error instanceof ToolError ? null : error instanceof Error ? error.message : String(error));
|
|
325
|
+
return emit({ signed_in: problem === null, base_url: credentials.baseUrl, ...(problem ? { error: problem } : {}) }, () => print(problem ?? `Signed in to ${credentials.baseUrl}.`));
|
|
321
326
|
}
|
|
322
327
|
throw new Error("Use auth login | logout | status.");
|
|
323
328
|
}
|
package/dist/oauth.d.ts
CHANGED
|
@@ -8,4 +8,6 @@ export interface StoredCredentials {
|
|
|
8
8
|
export declare function readCredentials(): StoredCredentials | null;
|
|
9
9
|
export declare function logout(): Promise<void>;
|
|
10
10
|
export declare function login(baseUrl?: string, openInBrowser?: boolean): Promise<void>;
|
|
11
|
+
/** What a user reads when the stored sign-in no longer works on the server. */
|
|
12
|
+
export declare const SIGN_IN_ENDED = "Your Ronana sign-in has ended (disconnected or expired). Run `ronana auth login` again.";
|
|
11
13
|
export declare function accessToken(): Promise<string>;
|
package/dist/oauth.js
CHANGED
|
@@ -84,24 +84,37 @@ export async function login(baseUrl = process.env.RONANA_BASE_URL ?? "https://ro
|
|
|
84
84
|
let fail;
|
|
85
85
|
const code = new Promise((ok, no) => { settle = ok; fail = no; });
|
|
86
86
|
const timer = setTimeout(() => fail(new Error("Sign-in timed out. Run `ronana auth login` again.")), LOGIN_TIMEOUT_MS);
|
|
87
|
-
|
|
87
|
+
/* The browser tab waits on this response until the code has been traded
|
|
88
|
+
for tokens, so it only says "connected" once that is true. */
|
|
89
|
+
let held = null;
|
|
90
|
+
let answered = false;
|
|
91
|
+
const finish = (text, status = 200) => {
|
|
92
|
+
if (!held)
|
|
93
|
+
return;
|
|
94
|
+
held.writeHead(status, { "Content-Type": "text/plain; charset=utf-8" }).end(text);
|
|
95
|
+
held = null;
|
|
96
|
+
};
|
|
97
|
+
const close = () => {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
finish("Sign-in did not finish. See your terminal for why.", 500);
|
|
100
|
+
server.close();
|
|
101
|
+
};
|
|
88
102
|
const server = createServer((req, res) => {
|
|
89
103
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
90
104
|
// 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
|
|
92
|
-
if (url.pathname !== "/callback" || url.searchParams.get("state") !== state) {
|
|
105
|
+
// earlier sign-in, a repeat) is ignored, so it cannot end the sign-in.
|
|
106
|
+
if (answered || url.pathname !== "/callback" || url.searchParams.get("state") !== state) {
|
|
93
107
|
res.writeHead(404).end();
|
|
94
108
|
return;
|
|
95
109
|
}
|
|
110
|
+
answered = true;
|
|
96
111
|
if (url.searchParams.get("error")) {
|
|
97
|
-
res.writeHead(400).end("Authorization was not completed.");
|
|
112
|
+
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" }).end("Authorization was not completed.");
|
|
98
113
|
fail(new Error(url.searchParams.get("error_description") ?? url.searchParams.get("error")));
|
|
114
|
+
return;
|
|
99
115
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
settle(url.searchParams.get("code") ?? "");
|
|
103
|
-
}
|
|
104
|
-
close();
|
|
116
|
+
held = res;
|
|
117
|
+
settle(url.searchParams.get("code") ?? "");
|
|
105
118
|
});
|
|
106
119
|
server.once("error", (error) => { clearTimeout(timer); reject(error); });
|
|
107
120
|
server.listen(0, "127.0.0.1", () => {
|
|
@@ -110,7 +123,7 @@ export async function login(baseUrl = process.env.RONANA_BASE_URL ?? "https://ro
|
|
|
110
123
|
close();
|
|
111
124
|
return reject(new Error("Loopback callback failed."));
|
|
112
125
|
}
|
|
113
|
-
resolve({ redirectUri: `http://127.0.0.1:${address.port}/callback`, code, close });
|
|
126
|
+
resolve({ redirectUri: `http://127.0.0.1:${address.port}/callback`, code, finish, close });
|
|
114
127
|
});
|
|
115
128
|
});
|
|
116
129
|
// Every exit below (refused registration, timeout, a refused token) frees the
|
|
@@ -170,7 +183,8 @@ ${authorize}
|
|
|
170
183
|
refreshToken: tokens.refresh_token,
|
|
171
184
|
expiresAt: Date.now() + Number(tokens.expires_in ?? 900) * 1000,
|
|
172
185
|
});
|
|
173
|
-
|
|
186
|
+
callback.finish("Ronana is connected. You can close this window.");
|
|
187
|
+
if (previous)
|
|
174
188
|
await revoke(previous);
|
|
175
189
|
}
|
|
176
190
|
finally {
|
|
@@ -214,6 +228,8 @@ async function withRefreshLock(fn) {
|
|
|
214
228
|
catch { /* Gone already. */ }
|
|
215
229
|
}
|
|
216
230
|
}
|
|
231
|
+
/** What a user reads when the stored sign-in no longer works on the server. */
|
|
232
|
+
export const SIGN_IN_ENDED = "Your Ronana sign-in has ended (disconnected or expired). Run `ronana auth login` again.";
|
|
217
233
|
export async function accessToken() {
|
|
218
234
|
const stored = readCredentials();
|
|
219
235
|
if (!stored)
|
|
@@ -237,8 +253,15 @@ export async function accessToken() {
|
|
|
237
253
|
// server's own words ("session not found") would read as something else.
|
|
238
254
|
if (error instanceof TypeError)
|
|
239
255
|
throw error;
|
|
240
|
-
|
|
256
|
+
// A login in another process replaces the stored sign-in and revokes the
|
|
257
|
+
// one we were refreshing: carry on with the new one instead.
|
|
258
|
+
const replaced = readCredentials();
|
|
259
|
+
if (replaced && replaced.clientId !== current.clientId && fresh(replaced))
|
|
260
|
+
return { replaced: replaced.accessToken };
|
|
261
|
+
throw new Error(SIGN_IN_ENDED);
|
|
241
262
|
});
|
|
263
|
+
if (typeof tokens.replaced === "string")
|
|
264
|
+
return tokens.replaced;
|
|
242
265
|
if (typeof tokens.access_token !== "string")
|
|
243
266
|
throw new Error("OAuth refresh failed.");
|
|
244
267
|
const next = {
|
package/dist/remote.js
CHANGED
|
@@ -6,7 +6,7 @@ import { readFile } from "node:fs/promises";
|
|
|
6
6
|
import { basename, extname } from "node:path";
|
|
7
7
|
import { Readable } from "node:stream";
|
|
8
8
|
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
9
|
-
import { accessToken, readCredentials } from "./oauth.js";
|
|
9
|
+
import { accessToken, readCredentials, SIGN_IN_ENDED } from "./oauth.js";
|
|
10
10
|
export async function remoteClient() {
|
|
11
11
|
const credentials = readCredentials();
|
|
12
12
|
if (!credentials)
|
|
@@ -20,6 +20,10 @@ export async function remoteClient() {
|
|
|
20
20
|
await client.connect(transport);
|
|
21
21
|
}
|
|
22
22
|
catch (error) {
|
|
23
|
+
// A 401 on a token that was still in date: the server answers that for a
|
|
24
|
+
// connection disconnected on the website.
|
|
25
|
+
if (error instanceof Error && /\b401\b|unauthori[sz]ed|revoked/i.test(error.message))
|
|
26
|
+
throw new Error(SIGN_IN_ENDED);
|
|
23
27
|
// A 404 means either an account outside the MCP rollout or a server with no
|
|
24
28
|
// MCP at all (a wrong --base-url). Only a server that advertises the MCP
|
|
25
29
|
// resource can be refusing the account.
|