@ronana/mcp 0.1.0 → 0.1.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/dist/oauth.js +64 -50
- package/package.json +2 -2
package/dist/oauth.js
CHANGED
|
@@ -8,6 +8,9 @@ import { Entry } from "@napi-rs/keyring";
|
|
|
8
8
|
const SERVICE = "Ronana MCP";
|
|
9
9
|
const ACCOUNT = "oauth";
|
|
10
10
|
const SCOPES = "mcp:read mcp:upload mcp:generate offline_access";
|
|
11
|
+
/** How long a sign-in may sit in the browser. An AI agent often runs login for
|
|
12
|
+
* the user, and a closed tab must end the command rather than hang it. */
|
|
13
|
+
const LOGIN_TIMEOUT_MS = 5 * 60_000;
|
|
11
14
|
const entry = () => new Entry(SERVICE, ACCOUNT);
|
|
12
15
|
export function readCredentials() {
|
|
13
16
|
const value = entry().getPassword();
|
|
@@ -68,6 +71,8 @@ export async function login(baseUrl = process.env.RONANA_BASE_URL ?? "https://ro
|
|
|
68
71
|
let settle;
|
|
69
72
|
let fail;
|
|
70
73
|
const code = new Promise((ok, no) => { settle = ok; fail = no; });
|
|
74
|
+
const timer = setTimeout(() => fail(new Error("Sign-in timed out. Run `ronana auth login` again.")), LOGIN_TIMEOUT_MS);
|
|
75
|
+
const close = () => { clearTimeout(timer); server.close(); };
|
|
71
76
|
const server = createServer((req, res) => {
|
|
72
77
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
73
78
|
if (url.searchParams.get("state") !== state) {
|
|
@@ -82,65 +87,74 @@ export async function login(baseUrl = process.env.RONANA_BASE_URL ?? "https://ro
|
|
|
82
87
|
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" }).end("Ronana is connected. You can close this window.");
|
|
83
88
|
settle(url.searchParams.get("code") ?? "");
|
|
84
89
|
}
|
|
85
|
-
|
|
90
|
+
close();
|
|
86
91
|
});
|
|
87
|
-
server.once("error", reject);
|
|
92
|
+
server.once("error", (error) => { clearTimeout(timer); reject(error); });
|
|
88
93
|
server.listen(0, "127.0.0.1", () => {
|
|
89
94
|
const address = server.address();
|
|
90
|
-
if (!address || typeof address === "string")
|
|
95
|
+
if (!address || typeof address === "string") {
|
|
96
|
+
close();
|
|
91
97
|
return reject(new Error("Loopback callback failed."));
|
|
92
|
-
|
|
98
|
+
}
|
|
99
|
+
resolve({ redirectUri: `http://127.0.0.1:${address.port}/callback`, code, close });
|
|
93
100
|
});
|
|
94
101
|
});
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
// Every exit below (refused registration, timeout, a refused token) frees the
|
|
103
|
+
// loopback port and the timer, so the process can end.
|
|
104
|
+
try {
|
|
105
|
+
const registration = await fetch(`${baseUrl}/api/auth/oauth2/register`, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: { "Content-Type": "application/json" },
|
|
108
|
+
body: JSON.stringify({
|
|
109
|
+
client_name: "Ronana CLI",
|
|
110
|
+
redirect_uris: [callback.redirectUri],
|
|
111
|
+
token_endpoint_auth_method: "none",
|
|
112
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
113
|
+
response_types: ["code"],
|
|
114
|
+
scope: SCOPES,
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
const registered = await registration.json();
|
|
118
|
+
if (!registration.ok || typeof registered.client_id !== "string")
|
|
119
|
+
throw new Error("OAuth client registration failed.");
|
|
120
|
+
const authorize = new URL(`${baseUrl}/api/auth/oauth2/authorize`);
|
|
121
|
+
authorize.search = new URLSearchParams({
|
|
122
|
+
response_type: "code",
|
|
123
|
+
client_id: registered.client_id,
|
|
124
|
+
redirect_uri: callback.redirectUri,
|
|
104
125
|
scope: SCOPES,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
authorize.search = new URLSearchParams({
|
|
112
|
-
response_type: "code",
|
|
113
|
-
client_id: registered.client_id,
|
|
114
|
-
redirect_uri: callback.redirectUri,
|
|
115
|
-
scope: SCOPES,
|
|
116
|
-
resource: `${baseUrl}/mcp`,
|
|
117
|
-
code_challenge: challenge,
|
|
118
|
-
code_challenge_method: "S256",
|
|
119
|
-
state,
|
|
120
|
-
}).toString();
|
|
121
|
-
process.stderr.write(`${openInBrowser ? "Opening your browser to sign in. If it does not open, visit" : "Open this link to sign in"}:
|
|
126
|
+
resource: `${baseUrl}/mcp`,
|
|
127
|
+
code_challenge: challenge,
|
|
128
|
+
code_challenge_method: "S256",
|
|
129
|
+
state,
|
|
130
|
+
}).toString();
|
|
131
|
+
process.stderr.write(`${openInBrowser ? "Opening your browser to sign in. If it does not open, visit" : "Open this link to sign in"}:
|
|
122
132
|
${authorize}
|
|
123
133
|
`);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
134
|
+
if (openInBrowser)
|
|
135
|
+
openBrowser(authorize.toString());
|
|
136
|
+
const code = await callback.code;
|
|
137
|
+
const tokens = await tokenRequest(baseUrl, {
|
|
138
|
+
grant_type: "authorization_code",
|
|
139
|
+
client_id: registered.client_id,
|
|
140
|
+
redirect_uri: callback.redirectUri,
|
|
141
|
+
code,
|
|
142
|
+
code_verifier: verifier,
|
|
143
|
+
resource: `${baseUrl}/mcp`,
|
|
144
|
+
});
|
|
145
|
+
if (typeof tokens.access_token !== "string" || typeof tokens.refresh_token !== "string")
|
|
146
|
+
throw new Error("OAuth response did not contain tokens.");
|
|
147
|
+
writeCredentials({
|
|
148
|
+
baseUrl,
|
|
149
|
+
clientId: registered.client_id,
|
|
150
|
+
accessToken: tokens.access_token,
|
|
151
|
+
refreshToken: tokens.refresh_token,
|
|
152
|
+
expiresAt: Date.now() + Number(tokens.expires_in ?? 900) * 1000,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
callback.close();
|
|
157
|
+
}
|
|
144
158
|
}
|
|
145
159
|
const fresh = (c) => c.expiresAt > Date.now() + 60_000;
|
|
146
160
|
/** Several CLI runs (and the MCP bridge) share one stored refresh token, and the
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ronana/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Ronana command line and local MCP bridge.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://ronana.ai",
|
|
7
7
|
"keywords": ["ronana", "mcp", "cli", "image generation", "video generation"],
|
|
8
8
|
"type": "module",
|
|
9
|
-
"bin": { "ronana": "
|
|
9
|
+
"bin": { "ronana": "dist/cli.js", "ronana-mcp": "dist/server.js" },
|
|
10
10
|
"files": ["dist", "README.md"],
|
|
11
11
|
"publishConfig": { "access": "public" },
|
|
12
12
|
"scripts": {
|