@chrischall/mcp-host-cli 0.0.0-stage → 2.33.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/src/login.mjs ADDED
@@ -0,0 +1,53 @@
1
+ // `connections login <principal id>`: run a connection's declared sign-in
2
+ // again (`authFlow`, core/src/auth-flow.ts) — the one repair for a credential
3
+ // that expired upstream short of revoking the connection, which loses the
4
+ // grant and its usage history.
5
+ //
6
+ // On a terminal it is the conversation `/authorize` would have had: each
7
+ // step's prompts asked here (a password without echo, the schema's default
8
+ // type), answered through `/login/step` until the flow ends. Off a terminal it
9
+ // prints nothing but the first answer and says how to continue with
10
+ // `connections login-step`, because a script cannot be prompted and should
11
+ // state its answers itself. Pulled out of index.mjs so the loop can be driven
12
+ // by a scripted prompter: the CLI's own tests run over a pipe and so only ever
13
+ // reach the non-interactive branch.
14
+
15
+ /** Every state a login can end in that is not success (core `loginStateSchema`). */
16
+ const FAILED = new Set(['rejected', 'unavailable', 'incoherent']);
17
+
18
+ /**
19
+ * @param {(method: string, path: string, body?: unknown) => Promise<any>} api
20
+ * @param {string} id
21
+ * @param {{ interactive: boolean, prompter?: import('./prompt.mjs').Prompter, stderr?: { write(s: string): unknown } }} opts
22
+ * @returns {Promise<{ result: any, exitCode: number }>}
23
+ */
24
+ export async function runLogin(api, id, { interactive, prompter, stderr = process.stderr }) {
25
+ const base = `/api/v1/principals/${encodeURIComponent(id)}/login`;
26
+ let result = await api('POST', base);
27
+ if (interactive && prompter) {
28
+ while (result.state === 'prompt') {
29
+ if (result.title) stderr.write(`${result.title}\n`);
30
+ if (result.description) stderr.write(`${result.description}\n`);
31
+ // Resolved by the server against the hosts the flow declared; absent
32
+ // when it was not on one, so there is nothing here to check.
33
+ if (result.link) stderr.write(`${result.link.label}: ${result.link.url}\n`);
34
+ /** @type {Record<string, string>} */
35
+ const answers = {};
36
+ for (const p of result.prompts ?? []) {
37
+ if (p.type === 'choice') answers[p.name] = await prompter.choose(p.label, p.options);
38
+ else if (p.type === 'text') answers[p.name] = await prompter.ask(p.label);
39
+ else answers[p.name] = await prompter.secret(p.label);
40
+ }
41
+ result = await api('POST', `${base}/step`, { login: result.login, step: result.step, answers });
42
+ }
43
+ return { result, exitCode: result.state === 'done' ? 0 : 1 };
44
+ }
45
+ if (result.state === 'prompt') {
46
+ stderr.write(
47
+ `mcp-host: continue with: mcp-host connections login-step ${id} --login ${result.login} --step ${result.step} --answers '{"<name>":"<value>"}'\n`,
48
+ );
49
+ }
50
+ // A prompt is not a failure off a terminal — it is the flow waiting for the
51
+ // scripted answer — but a refusal is, on either path.
52
+ return { result, exitCode: FAILED.has(result.state) ? 1 : 0 };
53
+ }