@rikaidev/yomi 0.3.1 → 0.4.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/PRIVACY.md +14 -0
- package/README.md +3 -0
- package/dist/auth/credential-store.js +11 -1
- package/dist/mcp/client-capabilities.js +55 -0
- package/dist/mcp/handlers/login-copy.js +200 -0
- package/dist/mcp/handlers/login-mrtr.js +150 -0
- package/dist/mcp/handlers/login-session.js +132 -0
- package/dist/mcp/handlers/login.js +67 -239
- package/dist/mcp/server.js +191 -229
- package/dist/mcp/tools.js +3 -14
- package/dist/mcp/ui/resource.js +3 -3
- package/dist/mcp/ui/tools-with-ui.js +3 -4
- package/dist/util/update-check.js +121 -0
- package/dist/version.js +1 -1
- package/package.json +3 -2
|
@@ -1,103 +1,24 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Yomi MCP `login`/`login_complete` handlers.
|
|
3
|
-
*
|
|
4
|
-
* Split out of handlers.ts purely to keep that file (the general tool
|
|
5
|
-
* handler surface) under the project's 500-scc-line module cap; behavior
|
|
6
|
-
* owned here is otherwise unchanged in spirit from what previously lived
|
|
7
|
-
* inline there.
|
|
8
|
-
*
|
|
9
|
-
* Two login paths exist, dispatched by client capability:
|
|
10
|
-
*
|
|
11
|
-
* - Elicitation clients: the original single-call flow — elicit phone/
|
|
12
|
-
* region, elicit PIN acknowledgement, block to completion. Unchanged.
|
|
13
|
-
*
|
|
14
|
-
* - Non-elicitation clients (e.g. Claude Desktop, confirmed empirically —
|
|
15
|
-
* it does not support MCP elicitation): the PIN has nowhere to go if we
|
|
16
|
-
* block, since it is produced mid-flow and stderr is invisible to the
|
|
17
|
-
* human, but a tool RESULT is visible. So the flow is split across two
|
|
18
|
-
* tool calls: `login` starts the passwordless flow and returns the PIN
|
|
19
|
-
* as soon as LINE issues it (bounded wait), then `login_complete`
|
|
20
|
-
* awaits the same in-flight login to finish once the human has acted
|
|
21
|
-
* on their phone.
|
|
22
|
-
*
|
|
23
|
-
* Both paths call the ONE shared login sequence in ../cli/login.ts
|
|
24
|
-
* (`runPwlessLogin`) — neither duplicates it.
|
|
25
|
-
*/
|
|
26
1
|
import { runPwlessLogin } from '../../cli/login.js';
|
|
27
2
|
import { LINE_PIN_CODE_LIFETIME_MS, PAAK_AUTH_CLIENT_CEILING_MS, PIN_VERIFY_CLIENT_CEILING_MS, } from '../../line/auth/pwless/index.js';
|
|
28
3
|
import { createCliLogger } from '../../util/log.js';
|
|
4
|
+
import { supportsFormElicitation } from '../client-capabilities.js';
|
|
29
5
|
import { supportsMcpApps } from '../ui/capability.js';
|
|
6
|
+
import { buildAcknowledgementForm, buildPreflightForm, certSteps, LOGIN_REGIONS, PRIMARY_DEVICE_SETTING_PATH, pinSteps, prerequisiteNotMetResult, readPreflightForm, } from './login-copy.js';
|
|
7
|
+
import { handleLoginMrtr } from './login-mrtr.js';
|
|
8
|
+
import { finishPendingLogin, getLivePendingLogin, getPendingLogin, PIN_WAIT_TIMEOUT_MS, startPendingLogin, waitForPin, } from './login-session.js';
|
|
30
9
|
import { jsonText, toolError } from './shared.js';
|
|
31
10
|
const log = createCliLogger('Yomi');
|
|
32
|
-
/** LINE region codes offered on the login elicitation form. */
|
|
33
|
-
const LOGIN_REGIONS = ['TW', 'JP', 'TH', 'ID', 'US'];
|
|
34
|
-
/**
|
|
35
|
-
* How long a live `pendingLogin` may be reused by a later `login` call for
|
|
36
|
-
* the same phone/region, before treating it as stale and starting fresh.
|
|
37
|
-
*
|
|
38
|
-
* This is bound to LINE's real PIN code lifetime (~line/auth/pwless/index.ts,
|
|
39
|
-
* an external fact from LINE Help Center, ~3 minutes) — NOT to how long
|
|
40
|
-
* yomi's client is willing to keep polling (~16 minutes). A login started
|
|
41
|
-
* 90 seconds ago is still very much alive and its PIN is still valid; the
|
|
42
|
-
* only thing that actually kills a pending login early is LINE's own code
|
|
43
|
-
* expiring, so that is the correct bound for "is this still worth reusing".
|
|
44
|
-
*/
|
|
45
|
-
const PIN_WINDOW_MS = LINE_PIN_CODE_LIFETIME_MS;
|
|
46
11
|
/**
|
|
47
12
|
* How long yomi's client keeps polling for the device-approval step once a
|
|
48
13
|
* PIN has been verified (or skipped via a stored certificate) — the real
|
|
49
|
-
* ceiling
|
|
14
|
+
* ceiling the completing call blocks for, used only to describe that wait to
|
|
50
15
|
* the human, not to bound anything server-side.
|
|
51
16
|
*/
|
|
52
17
|
const APPROVAL_WINDOW_MS = PAAK_AUTH_CLIENT_CEILING_MS;
|
|
53
|
-
/** How long `login` waits for LINE to issue a PIN before reporting a failure. */
|
|
54
|
-
const PIN_WAIT_TIMEOUT_MS = 20000;
|
|
55
|
-
/**
|
|
56
|
-
* Elicit `phone`/`region` from the human in one form, pre-filling whatever
|
|
57
|
-
* the caller already supplied.
|
|
58
|
-
*
|
|
59
|
-
* @param server - MCP Server instance (elicitation already capability-gated
|
|
60
|
-
* by the caller).
|
|
61
|
-
* @param phone - Phone already supplied by the tool call, if any.
|
|
62
|
-
* @param region - Region already supplied by the tool call, if any.
|
|
63
|
-
* @returns The collected phone/region, or `null` if the human declined/cancelled.
|
|
64
|
-
*/
|
|
65
|
-
async function elicitPhoneRegion(server, phone, region) {
|
|
66
|
-
const collected = await server.elicitInput({
|
|
67
|
-
message: 'Yomi needs your LINE phone number and region to start passwordless login.',
|
|
68
|
-
requestedSchema: {
|
|
69
|
-
type: 'object',
|
|
70
|
-
properties: {
|
|
71
|
-
phone: {
|
|
72
|
-
type: 'string',
|
|
73
|
-
title: 'Phone number',
|
|
74
|
-
description: 'E.164 form, e.g. +8869XXXXXXXX.',
|
|
75
|
-
...(phone ? { default: phone } : {}),
|
|
76
|
-
},
|
|
77
|
-
region: {
|
|
78
|
-
type: 'string',
|
|
79
|
-
title: 'Region',
|
|
80
|
-
enum: LOGIN_REGIONS,
|
|
81
|
-
...(region ? { default: region } : {}),
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
required: ['phone', 'region'],
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
if (collected.action !== 'accept') {
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
const collectedPhone = String(collected.content?.phone ?? '');
|
|
91
|
-
const collectedRegion = String(collected.content?.region ?? '');
|
|
92
|
-
if (!collectedPhone || !collectedRegion) {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
return { phone: collectedPhone, region: collectedRegion };
|
|
96
|
-
}
|
|
97
18
|
/**
|
|
98
19
|
* Elicit acknowledgement of the login PIN — fire-and-forget from the
|
|
99
|
-
* caller's perspective (must not block `
|
|
100
|
-
*
|
|
20
|
+
* caller's perspective (must not block `runPwlessLogin`, which is already
|
|
21
|
+
* running concurrently and waiting on phone biometric approval).
|
|
101
22
|
*
|
|
102
23
|
* @param server - MCP Server instance.
|
|
103
24
|
* @param pin - The PIN LINE just issued.
|
|
@@ -107,19 +28,7 @@ async function elicitPhoneRegion(server, phone, region) {
|
|
|
107
28
|
*/
|
|
108
29
|
function elicitPinAcknowledgement(server, pin, onDeclined) {
|
|
109
30
|
server
|
|
110
|
-
.elicitInput(
|
|
111
|
-
message: `LINE PIN: ${pin}\n\nEnter this PIN in the LINE app on your primary phone, then approve ` +
|
|
112
|
-
'the new device from there. Acknowledge below once you have done this.',
|
|
113
|
-
requestedSchema: {
|
|
114
|
-
type: 'object',
|
|
115
|
-
properties: {
|
|
116
|
-
entered: {
|
|
117
|
-
type: 'boolean',
|
|
118
|
-
title: 'I entered the PIN and approved the device on my phone',
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
})
|
|
31
|
+
.elicitInput(buildAcknowledgementForm(pin))
|
|
123
32
|
.then((result) => {
|
|
124
33
|
if (result.action !== 'accept') {
|
|
125
34
|
onDeclined();
|
|
@@ -132,12 +41,13 @@ function elicitPinAcknowledgement(server, pin, onDeclined) {
|
|
|
132
41
|
});
|
|
133
42
|
}
|
|
134
43
|
/**
|
|
135
|
-
* Handle `login`
|
|
136
|
-
* passwordless flow entirely through
|
|
137
|
-
*
|
|
44
|
+
* Handle `login` on a LEGACY-era connection whose client supports form
|
|
45
|
+
* elicitation — drive the passwordless flow entirely through pushed
|
|
46
|
+
* elicitation requests, with no out-of-band channel required. A pre-flight
|
|
47
|
+
* form confirms the 允許自其他裝置登入 prerequisite (and collects
|
|
48
|
+
* phone/region when missing) BEFORE any PIN is issued; the PIN is then
|
|
138
49
|
* surfaced through a second elicitation fired without blocking
|
|
139
|
-
* `
|
|
140
|
-
* phone. See ../cli/login.ts for the shared login sequence.
|
|
50
|
+
* `runPwlessLogin`, which keeps running while the human acts on their phone.
|
|
141
51
|
*
|
|
142
52
|
* @param server - MCP Server instance (for elicitInput).
|
|
143
53
|
* @param service - LineProtocolService (not yet authenticated).
|
|
@@ -145,15 +55,19 @@ function elicitPinAcknowledgement(server, pin, onDeclined) {
|
|
|
145
55
|
* @returns MCP tool result describing the logged-in profile.
|
|
146
56
|
*/
|
|
147
57
|
async function handleLoginElicitation(server, service, args) {
|
|
148
|
-
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
58
|
+
const collected = await server.elicitInput(buildPreflightForm(args.phone, args.region));
|
|
59
|
+
if (collected.action !== 'accept') {
|
|
60
|
+
return toolError('Login was cancelled by the user.');
|
|
61
|
+
}
|
|
62
|
+
const preflight = readPreflightForm(collected.content, args.phone, args.region);
|
|
63
|
+
if (!preflight.ok) {
|
|
64
|
+
if (preflight.reason === 'prerequisite') {
|
|
65
|
+
log.warn('login.prerequisite', { answer: preflight.answer });
|
|
66
|
+
return prerequisiteNotMetResult(preflight.answer);
|
|
153
67
|
}
|
|
154
|
-
;
|
|
155
|
-
({ phone, region } = collected);
|
|
68
|
+
return toolError('Login was cancelled by the user.');
|
|
156
69
|
}
|
|
70
|
+
const { phone, region } = preflight;
|
|
157
71
|
let pinDeclined = false;
|
|
158
72
|
const onPin = (pin) => {
|
|
159
73
|
log.warn('login.pin', { action: 'elicit_acknowledgement' });
|
|
@@ -188,90 +102,12 @@ async function handleLoginElicitation(server, service, args) {
|
|
|
188
102
|
return toolError(error?.message ?? String(error));
|
|
189
103
|
}
|
|
190
104
|
}
|
|
191
|
-
let pendingLogin = null;
|
|
192
|
-
/**
|
|
193
|
-
* Start a new passwordless login in the background and register it as the
|
|
194
|
-
* one in-flight pending login.
|
|
195
|
-
*
|
|
196
|
-
* The stored promise is the actual `runPwlessLogin` promise, so
|
|
197
|
-
* `login_complete` can await it and see the real success/failure. A
|
|
198
|
-
* `.catch()` is attached immediately (its result discarded, not stored) so
|
|
199
|
-
* an abandoned login — one nobody ever calls `login_complete` for — never
|
|
200
|
-
* surfaces as an unhandled rejection; this does not affect what
|
|
201
|
-
* `pendingLogin.promise` itself resolves/rejects with.
|
|
202
|
-
*
|
|
203
|
-
* @param service - LineProtocolService (not yet authenticated).
|
|
204
|
-
* @param phone - Phone number in E.164 form.
|
|
205
|
-
* @param region - Region code.
|
|
206
|
-
* @returns The newly registered pending-login record.
|
|
207
|
-
*/
|
|
208
|
-
function startPendingLogin(service, phone, region) {
|
|
209
|
-
let resolvePinReady = () => { };
|
|
210
|
-
const pinReady = new Promise((resolve) => {
|
|
211
|
-
resolvePinReady = resolve;
|
|
212
|
-
});
|
|
213
|
-
const pending = {
|
|
214
|
-
phone,
|
|
215
|
-
region,
|
|
216
|
-
pin: null,
|
|
217
|
-
certSkippedPin: false,
|
|
218
|
-
startedAt: Date.now(),
|
|
219
|
-
pinReady,
|
|
220
|
-
};
|
|
221
|
-
const onPin = (pin) => {
|
|
222
|
-
pending.pin = pin;
|
|
223
|
-
resolvePinReady();
|
|
224
|
-
};
|
|
225
|
-
const onBiometric = () => {
|
|
226
|
-
log.warn('login.waiting_biometric', {
|
|
227
|
-
action: 'approve the new device on your phone',
|
|
228
|
-
});
|
|
229
|
-
// Reaching biometric wait with no PIN ever set means a valid stored
|
|
230
|
-
// certificate skipped the PIN step entirely (see LinePwlessLogin.login,
|
|
231
|
-
// step 2-3) — resolve `pinReady` now so `waitForPin` doesn't block out
|
|
232
|
-
// its full timeout waiting for a PIN that will never arrive.
|
|
233
|
-
if (!pending.pin) {
|
|
234
|
-
pending.certSkippedPin = true;
|
|
235
|
-
resolvePinReady();
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
|
-
const promise = runPwlessLogin(service, phone, region, {
|
|
239
|
-
onPin,
|
|
240
|
-
onWaitingBiometric: onBiometric,
|
|
241
|
-
});
|
|
242
|
-
promise.catch(() => {
|
|
243
|
-
// Swallow here only to prevent an unhandled-rejection warning for a
|
|
244
|
-
// login nobody completes; the real error still surfaces to whoever
|
|
245
|
-
// awaits `pending.promise` (login_complete) below.
|
|
246
|
-
});
|
|
247
|
-
pending.promise = promise;
|
|
248
|
-
return pending;
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Wait for a pending login's PIN to arrive, bounded by a timeout.
|
|
252
|
-
*
|
|
253
|
-
* @param pending - The pending login to wait on.
|
|
254
|
-
* @param timeoutMs - Maximum time to wait.
|
|
255
|
-
* @returns The PIN, or `null` if it did not arrive within `timeoutMs`.
|
|
256
|
-
*/
|
|
257
|
-
async function waitForPin(pending, timeoutMs) {
|
|
258
|
-
if (pending.pin) {
|
|
259
|
-
return pending.pin;
|
|
260
|
-
}
|
|
261
|
-
const outcome = await Promise.race([
|
|
262
|
-
pending.pinReady.then(() => pending.pin ?? null),
|
|
263
|
-
new Promise((resolve) => {
|
|
264
|
-
setTimeout(() => resolve(null), timeoutMs);
|
|
265
|
-
}),
|
|
266
|
-
]);
|
|
267
|
-
return outcome;
|
|
268
|
-
}
|
|
269
105
|
/**
|
|
270
|
-
* Handle `login` for a client that
|
|
271
|
-
* (confirmed empirically for Claude Desktop). Splits the flow into two
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
106
|
+
* Handle `login` for a client that supports no form of elicitation at all
|
|
107
|
+
* (confirmed empirically for Claude Desktop). Splits the flow into two tool
|
|
108
|
+
* calls since the PIN cannot be prompted for in-band: this call starts (or
|
|
109
|
+
* reuses) the passwordless flow and returns the PIN as soon as LINE issues
|
|
110
|
+
* it; `login_complete` finishes it once the human has acted.
|
|
275
111
|
*
|
|
276
112
|
* The MCP Apps view (./ui/login-app.ts) has never been observed to render on
|
|
277
113
|
* any deployed client (see that file's header) — resources/read succeeds
|
|
@@ -322,6 +158,9 @@ async function handleLoginNoElicitation(service, args, supportsUi) {
|
|
|
322
158
|
type: 'text',
|
|
323
159
|
text: 'Ask the human for their LINE phone number (E.164 form, e.g. +8869XXXXXXXX) and region ' +
|
|
324
160
|
`(one of ${LOGIN_REGIONS.join(', ')}), then call \`login\` again with those as arguments. ` +
|
|
161
|
+
'In the same message, tell them yomi signs in as a SECOND device, which requires ' +
|
|
162
|
+
`${PRIMARY_DEVICE_SETTING_PATH} to be enabled on the phone already logged in — if it is ` +
|
|
163
|
+
'off, LINE never prompts that phone and the login cannot finish. ' +
|
|
325
164
|
'A login form may also appear for the human to fill in directly — if they use it, the ' +
|
|
326
165
|
'login proceeds on its own and there is nothing further for you to do — but do not wait ' +
|
|
327
166
|
'for that; if no form appears, asking the human directly is how this login makes progress.',
|
|
@@ -335,14 +174,13 @@ async function handleLoginNoElicitation(service, args, supportsUi) {
|
|
|
335
174
|
missing.push('phone');
|
|
336
175
|
if (!region)
|
|
337
176
|
missing.push('region');
|
|
338
|
-
return toolError(`Missing ${missing.join(' and ')} for login. This MCP client does not support elicitation, so ` +
|
|
177
|
+
return toolError(`Missing ${missing.join(' and ')} for login. This MCP client does not support form elicitation, so ` +
|
|
339
178
|
`${missing.join('/')} must be supplied as a tool argument, or persisted by running ` +
|
|
340
|
-
'`npx @rikaidev/yomi login` once in a terminal.'
|
|
179
|
+
'`npx @rikaidev/yomi login` once in a terminal. Also tell the human that yomi signs in as a ' +
|
|
180
|
+
`SECOND device, which requires ${PRIMARY_DEVICE_SETTING_PATH} enabled on the phone already ` +
|
|
181
|
+
'logged in — with it off, LINE never prompts that phone and no login can succeed.');
|
|
341
182
|
}
|
|
342
|
-
const
|
|
343
|
-
const active = pendingLogin && now - pendingLogin.startedAt < PIN_WINDOW_MS
|
|
344
|
-
? pendingLogin
|
|
345
|
-
: null;
|
|
183
|
+
const active = getLivePendingLogin(Date.now());
|
|
346
184
|
if (active && (active.phone !== phone || active.region !== region)) {
|
|
347
185
|
// A live login for a DIFFERENT number is still in flight. Never hand
|
|
348
186
|
// back its PIN under this call's identity, and never silently start a
|
|
@@ -352,10 +190,8 @@ async function handleLoginNoElicitation(service, args, supportsUi) {
|
|
|
352
190
|
'before starting a login for another number.');
|
|
353
191
|
}
|
|
354
192
|
// Reuse the in-flight login only when it matches this number; otherwise
|
|
355
|
-
// start a fresh one.
|
|
356
|
-
// the module-level `pendingLogin` is needed downstream.
|
|
193
|
+
// start a fresh one.
|
|
357
194
|
const pending = active ?? startPendingLogin(service, phone, region);
|
|
358
|
-
pendingLogin = pending;
|
|
359
195
|
const pin = await waitForPin(pending, PIN_WAIT_TIMEOUT_MS);
|
|
360
196
|
if (!pin) {
|
|
361
197
|
if (pending.certSkippedPin) {
|
|
@@ -368,14 +204,7 @@ async function handleLoginNoElicitation(service, args, supportsUi) {
|
|
|
368
204
|
pin: null,
|
|
369
205
|
clientApprovalPollCeilingSeconds: Math.round(APPROVAL_WINDOW_MS / 1000),
|
|
370
206
|
};
|
|
371
|
-
const
|
|
372
|
-
'so the PIN step was skipped.\n\n' +
|
|
373
|
-
'0. This only works if the primary phone has 設定 > 我的帳號 > 允許自其他裝置登入 ' +
|
|
374
|
-
'enabled — without it LINE will not offer this device a sign-in prompt at all.\n' +
|
|
375
|
-
'1. Bring the LINE app to the foreground on your primary phone — it should prompt you ' +
|
|
376
|
-
'to approve this new device signing in.\n' +
|
|
377
|
-
'2. Approve the device from there.\n\n';
|
|
378
|
-
const certText = certSteps +
|
|
207
|
+
const certText = certSteps() +
|
|
379
208
|
'Call the `login_complete` tool (no arguments) IMMEDIATELY now — do not wait for ' +
|
|
380
209
|
'confirmation that the approval happened first. `login_complete` blocks by itself while ' +
|
|
381
210
|
`you approve (up to about ${Math.round(APPROVAL_WINDOW_MS / 60000)} minutes) and returns ` +
|
|
@@ -399,16 +228,7 @@ async function handleLoginNoElicitation(service, args, supportsUi) {
|
|
|
399
228
|
clientPinPollCeilingSeconds: Math.round(PIN_VERIFY_CLIENT_CEILING_MS / 1000),
|
|
400
229
|
clientApprovalPollCeilingSeconds: Math.round(APPROVAL_WINDOW_MS / 1000),
|
|
401
230
|
};
|
|
402
|
-
const
|
|
403
|
-
` ${pin}\n\n` +
|
|
404
|
-
'0. This only works if the primary phone has 設定 > 我的帳號 > 允許自其他裝置登入 enabled — ' +
|
|
405
|
-
'without it LINE will not offer this device a sign-in prompt at all.\n' +
|
|
406
|
-
'1. Open LINE on the primary phone. If a verification prompt does not appear immediately, ' +
|
|
407
|
-
'bringing the app to the foreground surfaces it.\n' +
|
|
408
|
-
`2. Enter this PIN and tick the device that is signing in, then tap 「用戶確認」 — LINE gives ` +
|
|
409
|
-
`you ${pinCodeLifetimeSeconds} seconds (about 3 minutes) from when the code was shown to do this.\n` +
|
|
410
|
-
'3. LINE will then ask that same phone to approve this new device signing in — approve it there.\n\n';
|
|
411
|
-
const pinText = pinSteps +
|
|
231
|
+
const pinText = pinSteps(pin) +
|
|
412
232
|
`Call the \`login_complete\` tool (no arguments) IMMEDIATELY now — do not wait until you have ` +
|
|
413
233
|
'entered the PIN or approved the device. `login_complete` blocks by itself through both of ' +
|
|
414
234
|
`those steps: it keeps waiting up to about ${clientPinPollCeilingMinutes} minutes for step 2 and ` +
|
|
@@ -422,41 +242,50 @@ async function handleLoginNoElicitation(service, args, supportsUi) {
|
|
|
422
242
|
};
|
|
423
243
|
}
|
|
424
244
|
/**
|
|
425
|
-
* Handle `login` — dispatches to the
|
|
426
|
-
*
|
|
427
|
-
*
|
|
245
|
+
* Handle `login` — dispatches to the path the connected client can actually
|
|
246
|
+
* complete. See this module's header for the three paths.
|
|
247
|
+
*
|
|
248
|
+
* Elicitation is gated on the FORM sub-capability specifically, not on the
|
|
249
|
+
* presence of an `elicitation` object: since MCP 2026-07-28 a client may
|
|
250
|
+
* declare `elicitation: { url: {} }` alone, and both elicitation paths send
|
|
251
|
+
* a `requestedSchema` that such a client would reject. See
|
|
252
|
+
* ../client-capabilities.ts, which also owns WHERE the capabilities are read
|
|
253
|
+
* from — the accessor is empty on a modern-era stdio connection.
|
|
428
254
|
*
|
|
429
|
-
* @param server - MCP Server instance (for elicitInput
|
|
255
|
+
* @param server - MCP Server instance (for elicitInput on the legacy path).
|
|
430
256
|
* @param service - LineProtocolService (not yet authenticated).
|
|
431
257
|
* @param args - Tool arguments.
|
|
432
|
-
* @
|
|
258
|
+
* @param context - Era, client capabilities, and the multi-round-trip input
|
|
259
|
+
* responses a retried `tools/call` carried (`ctx.mcpReq.inputResponses`).
|
|
260
|
+
* @returns MCP tool result, or an `input_required` result on the MRTR path.
|
|
433
261
|
*/
|
|
434
|
-
export async function handleLogin(server, service, args) {
|
|
435
|
-
|
|
436
|
-
|
|
262
|
+
export async function handleLogin(server, service, args, context = {}) {
|
|
263
|
+
const { era = 'legacy', capabilities = server.getClientCapabilities(), inputResponses, } = context;
|
|
264
|
+
if (supportsFormElicitation(capabilities)) {
|
|
265
|
+
return era === 'legacy'
|
|
266
|
+
? handleLoginElicitation(server, service, args)
|
|
267
|
+
: handleLoginMrtr(service, args, inputResponses);
|
|
437
268
|
}
|
|
438
|
-
return handleLoginNoElicitation(service, args, supportsMcpApps(
|
|
269
|
+
return handleLoginNoElicitation(service, args, supportsMcpApps(capabilities));
|
|
439
270
|
}
|
|
440
271
|
/**
|
|
441
272
|
* Handle `login_complete` — finish a login started by a prior `login` call
|
|
442
|
-
* on a
|
|
443
|
-
* promise `login` started, so the real success/failure
|
|
444
|
-
*
|
|
273
|
+
* on a client with no elicitation. Awaits the same in-flight
|
|
274
|
+
* `runPwlessLogin` promise `login` started, so the real success/failure
|
|
275
|
+
* surfaces here.
|
|
445
276
|
*
|
|
446
277
|
* @returns MCP tool result describing the logged-in profile, or an honest error.
|
|
447
278
|
*/
|
|
448
279
|
export async function handleLoginComplete() {
|
|
449
|
-
|
|
280
|
+
const pending = getPendingLogin();
|
|
281
|
+
if (!pending) {
|
|
450
282
|
return toolError('No login is pending. Call `login` first to start a passwordless login. ' +
|
|
451
283
|
'(If you already called `login`, a previous attempt may have finished or its PIN code may have ' +
|
|
452
284
|
`expired — LINE only gives the human about ${Math.round(LINE_PIN_CODE_LIFETIME_MS / 1000)} seconds ` +
|
|
453
285
|
'(about 3 minutes) to act on a shown code. Calling `login` again is safe.)');
|
|
454
286
|
}
|
|
455
|
-
const pending = pendingLogin;
|
|
456
287
|
try {
|
|
457
|
-
const { mid, displayName } = await pending
|
|
458
|
-
pendingLogin = null;
|
|
459
|
-
log.info('login.complete', { mid });
|
|
288
|
+
const { mid, displayName } = await finishPendingLogin(pending);
|
|
460
289
|
return {
|
|
461
290
|
content: [
|
|
462
291
|
{
|
|
@@ -467,7 +296,6 @@ export async function handleLoginComplete() {
|
|
|
467
296
|
};
|
|
468
297
|
}
|
|
469
298
|
catch (error) {
|
|
470
|
-
pendingLogin = null;
|
|
471
299
|
return toolError(error?.message ?? String(error));
|
|
472
300
|
}
|
|
473
301
|
}
|