apifm-admin-mcp 26.5.3 → 26.5.4

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/README.md CHANGED
@@ -8,7 +8,7 @@ It is designed for Kiro, Cursor, Claude Code, Codex, Windsurf, Trae, Qoder, and
8
8
 
9
9
  Secrets must not be pasted into model chat.
10
10
 
11
- This MCP exposes `apifm_admin_start_auth`, which returns a one-time localhost URL. The user enters Basic Authentication credentials, an existing `X-Token`, username/password login details, email/password login details, or email signup details in that local browser page. Those secrets stay in the local MCP process memory and are never part of MCP tool arguments.
11
+ This MCP exposes `apifm_admin_start_auth`, which returns a one-time localhost URL. The user enters Basic Authentication credentials, an existing `X-Token`, username/password login details, email/password login details, email signup details, or mobile signup details in that local browser page. Those secrets stay in the local MCP process memory and are never part of MCP tool arguments.
12
12
 
13
13
  If an API call is attempted before authorization, `apifm_admin_call` and `apifm_admin_find_and_call` return the authorization URL directly in both visible text and structured content as `authUrl`. The agent should show that URL immediately instead of asking the user to call another tool.
14
14
 
@@ -53,7 +53,7 @@ If installed globally:
53
53
 
54
54
  ## Tools
55
55
 
56
- - `apifm_admin_start_auth`: Starts a local secure authorization page for username login, email login, Basic Auth, X-Token, or email registration. The page supports Simplified Chinese, Traditional Chinese, and English.
56
+ - `apifm_admin_start_auth`: Starts a local secure authorization page. The page separates "Log in" from "Register", supports username login, email login, Basic Auth, X-Token, email registration, and mobile registration. Basic Auth fields are merchant number and merchant key. The page supports Simplified Chinese, Traditional Chinese, and English.
57
57
  - `apifm_admin_accounts`: Lists local account aliases without revealing secrets.
58
58
  - `apifm_admin_switch_account`: Switches the active account alias.
59
59
  - `apifm_admin_remove_account`: Clears an account alias and its in-memory secrets.
@@ -68,7 +68,7 @@ For real backend data, agents should call `apifm_admin_find_and_call` or `apifm_
68
68
 
69
69
  1. User: "Log in to my admin account and read the user list."
70
70
  2. Agent calls `apifm_admin_find_and_call` with `query: "user list"` and a non-sensitive payload such as `{ "page": 1, "pageSize": 20 }`.
71
- 3. If not authorized yet, the tool returns `authUrl` immediately. The user opens that URL and chooses username login, email login, Basic Auth, X-Token, or email signup.
71
+ 3. If not authorized yet, the tool returns `authUrl` immediately. The user opens that URL and chooses a login method or a registration method.
72
72
  4. User completes authorization in the local browser page.
73
73
  5. Agent retries the same API call and answers from the returned `apiResult`, not from method documentation.
74
74
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apifm-admin-mcp",
3
- "version": "26.5.3",
3
+ "version": "26.5.4",
4
4
  "description": "MCP server for safely operating APIFM admin APIs through the apifm-admin SDK.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,156 +6,238 @@ import { getSdk, resetSdkConfig } from './sdk.js'
6
6
 
7
7
  const pendingSessions = new Map()
8
8
 
9
+ const AUTH_TYPES = new Set([
10
+ 'all',
11
+ 'basic',
12
+ 'x-token',
13
+ 'username-password',
14
+ 'email-password',
15
+ 'register-email',
16
+ 'register-mobile'
17
+ ])
18
+
19
+ const OPTIONAL_FIELDS = new Set([
20
+ 'alias',
21
+ 'pdomain',
22
+ 'imgcode',
23
+ 'k',
24
+ 'registerName',
25
+ 'registerType',
26
+ 'referrer',
27
+ 'agentKey',
28
+ 'mailCode',
29
+ 'smsCode',
30
+ 'smsImgCode',
31
+ 'smsK'
32
+ ])
33
+
9
34
  const I18N = {
10
35
  'zh-CN': {
11
36
  brand: 'APIFM Admin MCP',
12
- title: '安全连接你的 APIFM 后台',
37
+ title: '安全连接 APIFM 后台',
13
38
  lead: '在本机页面完成授权,密钥只保存在当前 MCP 进程内存,不会进入聊天上下文。',
14
39
  trustLocal: '本地 127.0.0.1 页面提交',
15
40
  trustMemory: '凭证仅在内存中保存',
16
41
  trustSwitch: '支持多个后台账号切换',
17
42
  footer: '授权完成后回到 Agent,重新执行刚才的后台操作。',
18
- formTitle: '选择授权方式',
43
+ formTitle: '授权方式',
19
44
  expires: '页面过期时间',
20
45
  alias: '账号别名',
21
46
  aliasPh: 'default-admin',
47
+ tabLogin: '登录现有账号',
48
+ tabRegister: '注册新账号',
22
49
  methodUsername: '用户名登录',
23
50
  methodUsernameSub: '用户名 + 密码',
24
51
  methodEmail: '邮箱登录',
25
52
  methodEmailSub: '邮箱 + 密码',
26
53
  methodBasic: 'Basic Auth',
27
- methodBasicSub: '用户名 + 密码',
54
+ methodBasicSub: '商户号 + 商户秘钥',
28
55
  methodToken: 'X-Token',
29
56
  methodTokenSub: '直接使用令牌',
30
- methodRegister: '邮箱注册',
31
- methodRegisterSub: '开通新后台',
57
+ methodRegisterEmail: '邮箱注册',
58
+ methodRegisterEmailSub: '邮箱验证码',
59
+ methodRegisterMobile: '手机号注册',
60
+ methodRegisterMobileSub: '短信验证码',
32
61
  usernameHint: '使用后台用户名和密码登录,MCP 会通过 SDK 换取 X-Token。',
33
62
  emailHint: '使用后台邮箱和密码登录,MCP 会通过 SDK 换取 X-Token。',
34
63
  basicHint: '填写 Basic Authentication 信息,将作为 Authorization 请求头调用后台接口。',
35
64
  tokenHint: '直接填写管理员登录后的 X-Token,用于后续所有后台接口调用。',
36
- registerHint: '使用邮箱注册开通新后台账号。需要验证码时,请先通过 SDK 获取邮箱验证码。',
65
+ registerEmailHint: '使用邮箱注册开通新后台账号,提交成功后 MCP 会尝试自动登录。',
66
+ registerMobileHint: '使用手机号注册开通新后台账号,提交成功后 MCP 会尝试自动登录。',
37
67
  username: '用户名',
38
68
  usernamePh: '请输入后台用户名',
39
69
  password: '密码',
40
70
  passwordPh: '请输入登录密码',
41
71
  pdomain: '专属域名,可选',
42
72
  pdomainPh: '例如 yourdomain',
73
+ imgcode: '图形验证码,可选',
74
+ imgcodePh: '请输入图形验证码',
75
+ k: '验证码随机数,可选',
76
+ kPh: '请输入验证码随机数',
43
77
  email: '邮箱地址',
44
78
  emailPh: 'name@example.com',
45
- basicUser: 'Basic 用户名',
46
- basicUserPh: '请输入 Basic 用户名',
47
- basicPass: 'Basic 密码',
48
- basicPassPh: '请输入 Basic 密码',
79
+ mobile: '手机号码',
80
+ mobilePh: '请输入手机号码',
81
+ merchantNo: '商户号',
82
+ merchantNoPh: '请输入商户号',
83
+ merchantKey: '商户秘钥',
84
+ merchantKeyPh: '请输入商户秘钥',
49
85
  xToken: 'X-Token',
50
86
  xTokenPh: '粘贴管理员 X-Token',
51
- newPassword: '登录密码',
52
- newPasswordPh: '设置登录密码',
87
+ registerPassword: '登录密码',
88
+ registerPasswordPh: '设置登录密码',
53
89
  name: '姓名或昵称',
54
90
  namePh: '可选',
55
91
  mailCode: '邮箱验证码',
56
- mailCodePh: '请输入验证码',
92
+ mailCodePh: '请输入邮箱验证码',
93
+ smsCode: '短信验证码',
94
+ smsCodePh: '请输入短信验证码',
95
+ type: '注册类型,可选',
96
+ typePh: '不填默认为 apifm',
97
+ referrer: '推荐人用户ID,可选',
98
+ referrerPh: '请输入推荐人用户ID',
99
+ agentKey: 'Agent Key,可选',
100
+ agentKeyPh: '请输入 agentKey',
57
101
  submit: '完成授权',
58
102
  security: '请不要把密码、Token 或 Basic Authentication 内容粘贴到聊天窗口。',
59
103
  closeNote: '提交成功后可以关闭本页面。'
60
104
  },
61
105
  'zh-TW': {
62
106
  brand: 'APIFM Admin MCP',
63
- title: '安全連接你的 APIFM 後台',
107
+ title: '安全連接 APIFM 後台',
64
108
  lead: '在本機頁面完成授權,密鑰只保存在目前 MCP 程序記憶體,不會進入聊天上下文。',
65
109
  trustLocal: '本地 127.0.0.1 頁面提交',
66
110
  trustMemory: '憑證僅在記憶體中保存',
67
111
  trustSwitch: '支援多個後台帳號切換',
68
112
  footer: '授權完成後回到 Agent,重新執行剛才的後台操作。',
69
- formTitle: '選擇授權方式',
113
+ formTitle: '授權方式',
70
114
  expires: '頁面過期時間',
71
115
  alias: '帳號別名',
72
116
  aliasPh: 'default-admin',
117
+ tabLogin: '登入現有帳號',
118
+ tabRegister: '註冊新帳號',
73
119
  methodUsername: '使用者名稱登入',
74
120
  methodUsernameSub: '使用者名稱 + 密碼',
75
121
  methodEmail: '郵箱登入',
76
122
  methodEmailSub: '郵箱 + 密碼',
77
123
  methodBasic: 'Basic Auth',
78
- methodBasicSub: '使用者名稱 + 密碼',
124
+ methodBasicSub: '商戶號 + 商戶秘鑰',
79
125
  methodToken: 'X-Token',
80
126
  methodTokenSub: '直接使用令牌',
81
- methodRegister: '郵箱註冊',
82
- methodRegisterSub: '開通新後台',
127
+ methodRegisterEmail: '郵箱註冊',
128
+ methodRegisterEmailSub: '郵箱驗證碼',
129
+ methodRegisterMobile: '手機號註冊',
130
+ methodRegisterMobileSub: '簡訊驗證碼',
83
131
  usernameHint: '使用後台使用者名稱和密碼登入,MCP 會透過 SDK 換取 X-Token。',
84
132
  emailHint: '使用後台郵箱和密碼登入,MCP 會透過 SDK 換取 X-Token。',
85
133
  basicHint: '填寫 Basic Authentication 資訊,將作為 Authorization 請求頭呼叫後台介面。',
86
134
  tokenHint: '直接填寫管理員登入後的 X-Token,用於後續所有後台介面呼叫。',
87
- registerHint: '使用郵箱註冊開通新後台帳號。需要驗證碼時,請先透過 SDK 取得郵箱驗證碼。',
135
+ registerEmailHint: '使用郵箱註冊開通新後台帳號,提交成功後 MCP 會嘗試自動登入。',
136
+ registerMobileHint: '使用手機號註冊開通新後台帳號,提交成功後 MCP 會嘗試自動登入。',
88
137
  username: '使用者名稱',
89
138
  usernamePh: '請輸入後台使用者名稱',
90
139
  password: '密碼',
91
140
  passwordPh: '請輸入登入密碼',
92
141
  pdomain: '專屬網域,可選',
93
142
  pdomainPh: '例如 yourdomain',
143
+ imgcode: '圖形驗證碼,可選',
144
+ imgcodePh: '請輸入圖形驗證碼',
145
+ k: '驗證碼隨機數,可選',
146
+ kPh: '請輸入驗證碼隨機數',
94
147
  email: '郵箱地址',
95
148
  emailPh: 'name@example.com',
96
- basicUser: 'Basic 使用者名稱',
97
- basicUserPh: '請輸入 Basic 使用者名稱',
98
- basicPass: 'Basic 密碼',
99
- basicPassPh: '請輸入 Basic 密碼',
149
+ mobile: '手機號碼',
150
+ mobilePh: '請輸入手機號碼',
151
+ merchantNo: '商戶號',
152
+ merchantNoPh: '請輸入商戶號',
153
+ merchantKey: '商戶秘鑰',
154
+ merchantKeyPh: '請輸入商戶秘鑰',
100
155
  xToken: 'X-Token',
101
156
  xTokenPh: '貼上管理員 X-Token',
102
- newPassword: '登入密碼',
103
- newPasswordPh: '設定登入密碼',
157
+ registerPassword: '登入密碼',
158
+ registerPasswordPh: '設定登入密碼',
104
159
  name: '姓名或暱稱',
105
160
  namePh: '可選',
106
161
  mailCode: '郵箱驗證碼',
107
- mailCodePh: '請輸入驗證碼',
162
+ mailCodePh: '請輸入郵箱驗證碼',
163
+ smsCode: '簡訊驗證碼',
164
+ smsCodePh: '請輸入簡訊驗證碼',
165
+ type: '註冊類型,可選',
166
+ typePh: '不填預設為 apifm',
167
+ referrer: '推薦人使用者ID,可選',
168
+ referrerPh: '請輸入推薦人使用者ID',
169
+ agentKey: 'Agent Key,可選',
170
+ agentKeyPh: '請輸入 agentKey',
108
171
  submit: '完成授權',
109
172
  security: '請不要把密碼、Token 或 Basic Authentication 內容貼到聊天視窗。',
110
173
  closeNote: '提交成功後可以關閉本頁面。'
111
174
  },
112
175
  en: {
113
176
  brand: 'APIFM Admin MCP',
114
- title: 'Connect your APIFM admin safely',
177
+ title: 'Connect APIFM admin safely',
115
178
  lead: 'Authorize on this local page. Secrets stay in this MCP process memory and never enter the chat transcript.',
116
179
  trustLocal: 'Submitted to local 127.0.0.1 only',
117
180
  trustMemory: 'Credentials are memory-only',
118
181
  trustSwitch: 'Multiple admin accounts supported',
119
182
  footer: 'After authorization, return to the agent and run the backend action again.',
120
- formTitle: 'Choose an authorization method',
183
+ formTitle: 'Authorization method',
121
184
  expires: 'Page expires at',
122
185
  alias: 'Account alias',
123
186
  aliasPh: 'default-admin',
187
+ tabLogin: 'Log in',
188
+ tabRegister: 'Register',
124
189
  methodUsername: 'Username login',
125
190
  methodUsernameSub: 'Username + password',
126
191
  methodEmail: 'Email login',
127
192
  methodEmailSub: 'Email + password',
128
193
  methodBasic: 'Basic Auth',
129
- methodBasicSub: 'Username + password',
194
+ methodBasicSub: 'Merchant no. + key',
130
195
  methodToken: 'X-Token',
131
196
  methodTokenSub: 'Use an existing token',
132
- methodRegister: 'Email signup',
133
- methodRegisterSub: 'Create new admin',
197
+ methodRegisterEmail: 'Email signup',
198
+ methodRegisterEmailSub: 'Email code',
199
+ methodRegisterMobile: 'Mobile signup',
200
+ methodRegisterMobileSub: 'SMS code',
134
201
  usernameHint: 'Log in with an admin username and password. The MCP will exchange them for an X-Token through the SDK.',
135
202
  emailHint: 'Log in with an admin email and password. The MCP will exchange them for an X-Token through the SDK.',
136
203
  basicHint: 'Enter Basic Authentication credentials. They will be sent as the Authorization header for API calls.',
137
204
  tokenHint: 'Enter an existing admin X-Token for later backend API calls.',
138
- registerHint: 'Create a new admin account by email. If a verification code is required, request it through the SDK first.',
205
+ registerEmailHint: 'Create a new admin account by email. After signup, the MCP will try to log in automatically.',
206
+ registerMobileHint: 'Create a new admin account by mobile. After signup, the MCP will try to log in automatically.',
139
207
  username: 'Username',
140
208
  usernamePh: 'Enter admin username',
141
209
  password: 'Password',
142
210
  passwordPh: 'Enter login password',
143
211
  pdomain: 'Private domain, optional',
144
212
  pdomainPh: 'Example: yourdomain',
213
+ imgcode: 'Image code, optional',
214
+ imgcodePh: 'Enter image code',
215
+ k: 'Captcha nonce, optional',
216
+ kPh: 'Enter captcha nonce',
145
217
  email: 'Email address',
146
218
  emailPh: 'name@example.com',
147
- basicUser: 'Basic username',
148
- basicUserPh: 'Enter Basic username',
149
- basicPass: 'Basic password',
150
- basicPassPh: 'Enter Basic password',
219
+ mobile: 'Mobile number',
220
+ mobilePh: 'Enter mobile number',
221
+ merchantNo: 'Merchant number',
222
+ merchantNoPh: 'Enter merchant number',
223
+ merchantKey: 'Merchant key',
224
+ merchantKeyPh: 'Enter merchant key',
151
225
  xToken: 'X-Token',
152
226
  xTokenPh: 'Paste admin X-Token',
153
- newPassword: 'Login password',
154
- newPasswordPh: 'Set login password',
227
+ registerPassword: 'Login password',
228
+ registerPasswordPh: 'Set login password',
155
229
  name: 'Name or nickname',
156
230
  namePh: 'Optional',
157
231
  mailCode: 'Email verification code',
158
- mailCodePh: 'Enter verification code',
232
+ mailCodePh: 'Enter email code',
233
+ smsCode: 'SMS verification code',
234
+ smsCodePh: 'Enter SMS code',
235
+ type: 'Signup type, optional',
236
+ typePh: 'Defaults to apifm',
237
+ referrer: 'Referrer user ID, optional',
238
+ referrerPh: 'Enter referrer user ID',
239
+ agentKey: 'Agent Key, optional',
240
+ agentKeyPh: 'Enter agentKey',
159
241
  submit: 'Authorize account',
160
242
  security: 'Do not paste passwords, tokens, or Basic Authentication values into the chat window.',
161
243
  closeNote: 'You can close this page after authorization succeeds.'
@@ -224,13 +306,14 @@ export async function createAuthSession({ authType = 'all', alias = '', domains
224
306
 
225
307
  async function finishAuth({ authType, alias, domains, fields }) {
226
308
  const selectedAuthType = normalizeAuthType(authType)
309
+
227
310
  if (selectedAuthType === 'basic') {
228
- const username = required(fields.basicUsername, 'Basic username')
229
- const password = required(fields.basicPassword, 'Basic password')
311
+ const merchantNo = required(fields.basicUsername, 'merchant number')
312
+ const merchantKey = required(fields.basicPassword, 'merchant key')
230
313
  return upsertAccount({
231
314
  alias: fields.alias || alias,
232
315
  authType: 'basic',
233
- basicAuth: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`,
316
+ basicAuth: `Basic ${Buffer.from(`${merchantNo}:${merchantKey}`).toString('base64')}`,
234
317
  domains
235
318
  })
236
319
  }
@@ -244,36 +327,8 @@ async function finishAuth({ authType, alias, domains, fields }) {
244
327
  })
245
328
  }
246
329
 
247
- if (selectedAuthType === 'username-password' || selectedAuthType === 'email-password' || selectedAuthType === 'password') {
248
- const loginMode = selectedAuthType === 'email-password' ? 'email' : 'username'
249
- const loginPayload =
250
- loginMode === 'email'
251
- ? {
252
- email: required(fields.loginEmail, 'email'),
253
- pwd: required(fields.loginPassword, 'password'),
254
- rememberMe: true
255
- }
256
- : {
257
- userName: required(fields.loginUsername, 'username'),
258
- pwd: required(fields.loginPassword, 'password'),
259
- rememberMe: true,
260
- pdomain: fields.pdomain || undefined
261
- }
262
-
263
- const sdk = getSdk()
264
- resetSdkConfig()
265
- if (domains && Object.keys(domains).length) {
266
- sdk.setDomains(domains)
267
- }
268
- const methodName = loginMode === 'email' ? 'loginAdminEmail' : 'loginAdminUserName'
269
- if (typeof sdk[methodName] !== 'function') {
270
- throw new Error(`apifm-admin does not expose ${methodName}`)
271
- }
272
- const result = await sdk[methodName](loginPayload)
273
- const token = extractToken(result)
274
- if (!token) {
275
- throw new Error('Login succeeded but no X-Token was found in the SDK response.')
276
- }
330
+ if (selectedAuthType === 'username-password' || selectedAuthType === 'email-password') {
331
+ const token = await loginAndExtractToken({ authType: selectedAuthType, domains, fields })
277
332
  return upsertAccount({
278
333
  alias: fields.alias || alias,
279
334
  authType: selectedAuthType,
@@ -283,18 +338,41 @@ async function finishAuth({ authType, alias, domains, fields }) {
283
338
  }
284
339
 
285
340
  if (selectedAuthType === 'register-email') {
286
- const sdk = getSdk()
287
- resetSdkConfig()
288
- if (domains && Object.keys(domains).length) {
289
- sdk.setDomains(domains)
290
- }
291
- const result = await sdk.registerAdminSaveEmail({
292
- email: required(fields.registerEmail, 'email'),
293
- pwd: required(fields.registerPassword, 'password'),
294
- name: fields.registerName || undefined,
295
- mailCode: fields.mailCode || fields.code || undefined
341
+ await callSdkMethod({
342
+ methodName: 'registerAdminSaveEmail',
343
+ domains,
344
+ payload: {
345
+ email: required(fields.registerEmail, 'email'),
346
+ pwd: required(fields.registerPassword, 'password'),
347
+ name: fields.registerName || undefined,
348
+ mailCode: fields.mailCode || undefined,
349
+ referrer: fields.referrer || undefined
350
+ }
351
+ })
352
+ const token = await loginAndExtractToken({ authType: 'email-password', domains, fields })
353
+ return upsertAccount({
354
+ alias: fields.alias || alias,
355
+ authType: selectedAuthType,
356
+ token,
357
+ domains
296
358
  })
297
- const token = extractToken(result)
359
+ }
360
+
361
+ if (selectedAuthType === 'register-mobile') {
362
+ await callSdkMethod({
363
+ methodName: 'registerAdminSave',
364
+ domains,
365
+ payload: {
366
+ type: fields.registerType || undefined,
367
+ mobile: required(fields.registerMobile, 'mobile'),
368
+ pwd: required(fields.registerPassword, 'password'),
369
+ name: fields.registerName || undefined,
370
+ smsCode: fields.smsCode || undefined,
371
+ referrer: fields.referrer || undefined,
372
+ agentKey: fields.agentKey || undefined
373
+ }
374
+ })
375
+ const token = await loginAndExtractToken({ authType: 'mobile-password', domains, fields })
298
376
  return upsertAccount({
299
377
  alias: fields.alias || alias,
300
378
  authType: selectedAuthType,
@@ -306,28 +384,141 @@ async function finishAuth({ authType, alias, domains, fields }) {
306
384
  throw new Error(`Unsupported auth type: ${authType}`)
307
385
  }
308
386
 
387
+ async function loginAndExtractToken({ authType, domains, fields }) {
388
+ const loginConfig = {
389
+ 'username-password': {
390
+ methodName: 'loginAdminUserName',
391
+ payload: {
392
+ userName: required(fields.loginUsername, 'username'),
393
+ pwd: required(fields.loginPassword, 'password'),
394
+ rememberMe: true,
395
+ pdomain: fields.pdomain || undefined,
396
+ imgcode: fields.imgcode || undefined,
397
+ k: fields.k || undefined
398
+ }
399
+ },
400
+ 'email-password': {
401
+ methodName: 'loginAdminEmail',
402
+ payload: {
403
+ email: required(fields.loginEmail || fields.registerEmail, 'email'),
404
+ pwd: required(fields.loginPassword || fields.registerPassword, 'password'),
405
+ rememberMe: true,
406
+ imgcode: fields.imgcode || undefined,
407
+ k: fields.k || undefined
408
+ }
409
+ },
410
+ 'mobile-password': {
411
+ methodName: 'loginAdminMobile',
412
+ payload: {
413
+ mobile: required(fields.registerMobile, 'mobile'),
414
+ pwd: required(fields.registerPassword, 'password'),
415
+ rememberMe: true,
416
+ imgcode: fields.smsImgCode || fields.imgcode || undefined,
417
+ k: fields.smsK || fields.k || undefined
418
+ }
419
+ }
420
+ }[authType]
421
+
422
+ const result = await callSdkMethod({
423
+ methodName: loginConfig.methodName,
424
+ domains,
425
+ payload: loginConfig.payload
426
+ })
427
+ const token = extractToken(result)
428
+ if (!token) {
429
+ throw new Error(
430
+ `${loginConfig.methodName} did not return an X-Token. Response shape was: ${JSON.stringify(maskResponseForError(result)).slice(0, 600)}`
431
+ )
432
+ }
433
+ return token
434
+ }
435
+
436
+ async function callSdkMethod({ methodName, domains, payload }) {
437
+ const sdk = getSdk()
438
+ resetSdkConfig()
439
+ if (domains && Object.keys(domains).length) {
440
+ sdk.setDomains(domains)
441
+ }
442
+ if (typeof sdk[methodName] !== 'function') {
443
+ throw new Error(`apifm-admin does not expose ${methodName}`)
444
+ }
445
+ return sdk[methodName](compactObject(payload))
446
+ }
447
+
448
+ function compactObject(input) {
449
+ return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined && value !== ''))
450
+ }
451
+
309
452
  function normalizeAuthType(authType) {
310
453
  if (authType === 'password') return 'username-password'
311
- if (
312
- ['all', 'basic', 'x-token', 'username-password', 'email-password', 'register-email'].includes(authType)
313
- ) {
314
- return authType
315
- }
454
+ if (AUTH_TYPES.has(authType)) return authType
316
455
  return 'all'
317
456
  }
318
457
 
319
458
  function extractToken(response) {
320
- const candidates = [
321
- response?.data?.token,
322
- response?.data?.xToken,
323
- response?.data?.xtoken,
324
- response?.data?.x_token,
325
- response?.data?.['x-token'],
326
- response?.token,
327
- response?.xToken,
328
- response?.xtoken
459
+ const directPaths = [
460
+ ['data', 'token'],
461
+ ['data', 'xToken'],
462
+ ['data', 'xtoken'],
463
+ ['data', 'x_token'],
464
+ ['data', 'x-token'],
465
+ ['data', 'X-Token'],
466
+ ['data', 'loginToken'],
467
+ ['data', 'adminToken'],
468
+ ['token'],
469
+ ['xToken'],
470
+ ['xtoken'],
471
+ ['x_token'],
472
+ ['x-token'],
473
+ ['X-Token']
329
474
  ]
330
- return candidates.find((item) => typeof item === 'string' && item.trim()) || ''
475
+
476
+ for (const path of directPaths) {
477
+ const value = getPath(response, path)
478
+ if (typeof value === 'string' && value.trim()) return value.trim()
479
+ }
480
+
481
+ const found = findTokenDeep(response)
482
+ return found || ''
483
+ }
484
+
485
+ function getPath(value, path) {
486
+ return path.reduce((current, key) => (current && typeof current === 'object' ? current[key] : undefined), value)
487
+ }
488
+
489
+ function findTokenDeep(value, depth = 0) {
490
+ if (!value || typeof value !== 'object' || depth > 5) return ''
491
+ if (Array.isArray(value)) {
492
+ for (const item of value) {
493
+ const found = findTokenDeep(item, depth + 1)
494
+ if (found) return found
495
+ }
496
+ return ''
497
+ }
498
+ for (const [key, child] of Object.entries(value)) {
499
+ const compactKey = key.toLowerCase().replace(/[^a-z0-9]/g, '')
500
+ if (
501
+ typeof child === 'string' &&
502
+ child.trim() &&
503
+ ['token', 'xtoken', 'xToken', 'xstoken', 'admintoken', 'logintoken'].map((item) => item.toLowerCase()).includes(compactKey)
504
+ ) {
505
+ return child.trim()
506
+ }
507
+ const found = findTokenDeep(child, depth + 1)
508
+ if (found) return found
509
+ }
510
+ return ''
511
+ }
512
+
513
+ function maskResponseForError(value) {
514
+ if (!value || typeof value !== 'object') return value
515
+ if (Array.isArray(value)) return value.map(maskResponseForError)
516
+ return Object.fromEntries(
517
+ Object.entries(value).map(([key, child]) => [
518
+ key,
519
+ /token|password|pwd|secret|key/i.test(key) ? '[REDACTED]' : maskResponseForError(child)
520
+ ])
521
+ )
331
522
  }
332
523
 
333
524
  function required(value, label) {
@@ -373,6 +564,7 @@ function escapeHtml(value) {
373
564
 
374
565
  function renderForm({ authType, alias, expiresAt }) {
375
566
  const initialType = authType === 'all' ? 'username-password' : authType
567
+ const initialGroup = initialType.startsWith('register-') ? 'register' : 'login'
376
568
  return `<!doctype html>
377
569
  <html lang="zh-CN">
378
570
  <head>
@@ -392,8 +584,6 @@ function renderForm({ authType, alias, expiresAt }) {
392
584
  --accent-ink: oklch(1 0 0);
393
585
  --accent-soft: oklch(0.94 0.035 255);
394
586
  --success: oklch(0.58 0.15 155);
395
- --danger: oklch(0.55 0.18 25);
396
- --radius: 12px;
397
587
  }
398
588
  * { box-sizing: border-box; }
399
589
  body {
@@ -406,7 +596,7 @@ function renderForm({ authType, alias, expiresAt }) {
406
596
  color: var(--ink);
407
597
  }
408
598
  .shell {
409
- width: min(1080px, calc(100vw - 32px));
599
+ width: min(1120px, calc(100vw - 32px));
410
600
  min-height: 100vh;
411
601
  margin: 0 auto;
412
602
  display: grid;
@@ -416,7 +606,7 @@ function renderForm({ authType, alias, expiresAt }) {
416
606
  .panel {
417
607
  width: 100%;
418
608
  display: grid;
419
- grid-template-columns: minmax(280px, 0.84fr) minmax(320px, 1.16fr);
609
+ grid-template-columns: minmax(280px, 0.78fr) minmax(340px, 1.22fr);
420
610
  background: var(--surface);
421
611
  border: 1px solid var(--line);
422
612
  border-radius: 16px;
@@ -449,25 +639,35 @@ function renderForm({ authType, alias, expiresAt }) {
449
639
  .trust li { display: flex; gap: 10px; align-items: flex-start; color: oklch(0.91 0.015 250); }
450
640
  .dot { width: 8px; height: 8px; margin-top: 7px; border-radius: 999px; background: oklch(0.78 0.16 155); flex: 0 0 auto; }
451
641
  .content { padding: 30px; }
452
- .topbar { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom: 22px; }
642
+ .topbar { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom: 20px; }
453
643
  .meta { color: var(--muted); font-size: 0.92rem; }
454
- .lang { display: inline-grid; grid-auto-flow: column; gap: 4px; padding: 4px; background: var(--surface-2); border: 1px solid var(--line); border-radius: 999px; }
455
- .lang button, .method button {
644
+ .lang, .tabs {
645
+ display: inline-grid;
646
+ grid-auto-flow: column;
647
+ gap: 4px;
648
+ padding: 4px;
649
+ background: var(--surface-2);
650
+ border: 1px solid var(--line);
651
+ border-radius: 999px;
652
+ }
653
+ .lang button, .tabs button, .method button {
456
654
  appearance: none;
457
655
  border: 0;
458
656
  font: inherit;
459
657
  cursor: pointer;
460
658
  }
461
- .lang button { padding: 6px 10px; border-radius: 999px; color: var(--muted); background: transparent; }
462
- .lang button[aria-pressed="true"] { background: var(--ink); color: white; }
659
+ .lang button, .tabs button { padding: 7px 12px; border-radius: 999px; color: var(--muted); background: transparent; }
660
+ .lang button[aria-pressed="true"], .tabs button[aria-pressed="true"] { background: var(--ink); color: white; }
661
+ .tabs { margin-bottom: 14px; }
463
662
  .method {
464
663
  display: grid;
465
- grid-template-columns: repeat(5, minmax(0, 1fr));
664
+ grid-template-columns: repeat(4, minmax(0, 1fr));
466
665
  gap: 8px;
467
666
  margin-bottom: 22px;
468
667
  }
668
+ .method[data-group="register"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
469
669
  .method button {
470
- min-height: 58px;
670
+ min-height: 62px;
471
671
  padding: 10px;
472
672
  border: 1px solid var(--line);
473
673
  border-radius: 10px;
@@ -519,11 +719,7 @@ function renderForm({ authType, alias, expiresAt }) {
519
719
  transition: transform 160ms ease, filter 160ms ease;
520
720
  }
521
721
  .submit:hover { filter: brightness(1.03); transform: translateY(-1px); }
522
- .note {
523
- margin: 12px 0 0;
524
- color: var(--muted);
525
- font-size: 0.9rem;
526
- }
722
+ .note { margin: 12px 0 0; color: var(--muted); font-size: 0.9rem; }
527
723
  .security {
528
724
  margin-top: 20px;
529
725
  padding: 12px 14px;
@@ -533,17 +729,17 @@ function renderForm({ authType, alias, expiresAt }) {
533
729
  font-size: 0.9rem;
534
730
  }
535
731
  [hidden] { display: none !important; }
536
- @media (max-width: 860px) {
732
+ @media (max-width: 900px) {
537
733
  .panel { grid-template-columns: 1fr; }
538
734
  .aside { padding: 26px; }
539
735
  .method { grid-template-columns: repeat(2, minmax(0, 1fr)); }
540
736
  .grid { grid-template-columns: 1fr; }
541
737
  }
542
738
  @media (max-width: 520px) {
543
- .shell { width: min(100vw - 20px, 1080px); padding: 10px 0; }
739
+ .shell { width: min(100vw - 20px, 1120px); padding: 10px 0; }
544
740
  .content { padding: 20px; }
545
741
  .topbar { align-items: flex-start; flex-direction: column; }
546
- .method { grid-template-columns: 1fr; }
742
+ .method, .method[data-group="register"] { grid-template-columns: 1fr; }
547
743
  h1 { font-size: 1.55rem; }
548
744
  }
549
745
  @media (prefers-reduced-motion: reduce) {
@@ -556,22 +752,22 @@ function renderForm({ authType, alias, expiresAt }) {
556
752
  <section class="panel" aria-labelledby="title">
557
753
  <aside class="aside">
558
754
  <div>
559
- <div class="brand"><span class="mark">A</span><span data-i18n="brand">APIFM Admin MCP</span></div>
560
- <h1 id="title" data-i18n="title">安全连接你的 APIFM 后台</h1>
561
- <p class="lead" data-i18n="lead">在本机页面完成授权,密钥只保存在当前 MCP 进程内存,不会进入聊天上下文。</p>
755
+ <div class="brand"><span class="mark">A</span><span data-i18n="brand"></span></div>
756
+ <h1 id="title" data-i18n="title"></h1>
757
+ <p class="lead" data-i18n="lead"></p>
562
758
  <ul class="trust">
563
- <li><span class="dot"></span><span data-i18n="trustLocal">本地 127.0.0.1 页面提交</span></li>
564
- <li><span class="dot"></span><span data-i18n="trustMemory">凭证仅在内存中保存</span></li>
565
- <li><span class="dot"></span><span data-i18n="trustSwitch">支持多个后台账号切换</span></li>
759
+ <li><span class="dot"></span><span data-i18n="trustLocal"></span></li>
760
+ <li><span class="dot"></span><span data-i18n="trustMemory"></span></li>
761
+ <li><span class="dot"></span><span data-i18n="trustSwitch"></span></li>
566
762
  </ul>
567
763
  </div>
568
- <p class="lead" data-i18n="footer">授权完成后回到 Agent,重新执行刚才的后台操作。</p>
764
+ <p class="lead" data-i18n="footer"></p>
569
765
  </aside>
570
766
  <div class="content">
571
767
  <div class="topbar">
572
768
  <div>
573
- <strong data-i18n="formTitle">选择授权方式</strong>
574
- <div class="meta"><span data-i18n="expires">页面过期时间</span>: ${escapeHtml(new Date(expiresAt).toLocaleString())}</div>
769
+ <strong data-i18n="formTitle"></strong>
770
+ <div class="meta"><span data-i18n="expires"></span>: ${escapeHtml(new Date(expiresAt).toLocaleString())}</div>
575
771
  </div>
576
772
  <div class="lang" aria-label="Language">
577
773
  <button type="button" data-lang="zh-CN" aria-pressed="true">简</button>
@@ -580,112 +776,57 @@ function renderForm({ authType, alias, expiresAt }) {
580
776
  </div>
581
777
  </div>
582
778
 
583
- <div class="method" role="tablist" aria-label="Auth methods">
779
+ <div class="tabs" role="tablist" aria-label="Account mode">
780
+ <button type="button" data-group-tab="login" aria-pressed="${initialGroup === 'login'}" data-i18n="tabLogin"></button>
781
+ <button type="button" data-group-tab="register" aria-pressed="${initialGroup === 'register'}" data-i18n="tabRegister"></button>
782
+ </div>
783
+
784
+ <div class="method" data-group="login">
584
785
  ${renderMethodButton('username-password', initialType)}
585
786
  ${renderMethodButton('email-password', initialType)}
586
787
  ${renderMethodButton('basic', initialType)}
587
788
  ${renderMethodButton('x-token', initialType)}
789
+ </div>
790
+ <div class="method" data-group="register" hidden>
588
791
  ${renderMethodButton('register-email', initialType)}
792
+ ${renderMethodButton('register-mobile', initialType)}
589
793
  </div>
590
794
 
591
795
  <form method="post" autocomplete="off" id="authForm">
592
796
  <input type="hidden" id="authType" name="authType" value="${escapeHtml(initialType)}">
593
797
  <div class="field">
594
- <label for="alias" data-i18n="alias">账号别名</label>
595
- <input id="alias" name="alias" value="${escapeHtml(alias)}" placeholder="default-admin" data-i18n-placeholder="aliasPh">
798
+ <label for="alias" data-i18n="alias"></label>
799
+ <input id="alias" name="alias" value="${escapeHtml(alias)}" data-i18n-placeholder="aliasPh">
596
800
  </div>
597
801
 
598
- <section class="section" data-section="username-password">
599
- <p class="section-head" data-i18n="usernameHint">使用后台用户名和密码登录,MCP 会通过 SDK 换取 X-Token。</p>
600
- <div class="grid">
601
- <div class="field">
602
- <label for="loginUsername" data-i18n="username">用户名</label>
603
- <input id="loginUsername" name="loginUsername" data-i18n-placeholder="usernamePh">
604
- </div>
605
- <div class="field">
606
- <label for="loginPasswordUser" data-i18n="password">密码</label>
607
- <input id="loginPasswordUser" name="loginPassword" type="password" data-i18n-placeholder="passwordPh">
608
- </div>
609
- </div>
610
- <div class="field" style="margin-top:14px">
611
- <label for="pdomain" data-i18n="pdomain">专属域名,可选</label>
612
- <input id="pdomain" name="pdomain" data-i18n-placeholder="pdomainPh">
613
- </div>
614
- </section>
615
-
616
- <section class="section" data-section="email-password">
617
- <p class="section-head" data-i18n="emailHint">使用后台邮箱和密码登录,MCP 会通过 SDK 换取 X-Token。</p>
618
- <div class="grid">
619
- <div class="field">
620
- <label for="loginEmail" data-i18n="email">邮箱地址</label>
621
- <input id="loginEmail" name="loginEmail" type="email" data-i18n-placeholder="emailPh">
622
- </div>
623
- <div class="field">
624
- <label for="loginPasswordEmail" data-i18n="password">密码</label>
625
- <input id="loginPasswordEmail" name="loginPassword" type="password" data-i18n-placeholder="passwordPh">
626
- </div>
627
- </div>
628
- </section>
629
-
630
- <section class="section" data-section="basic">
631
- <p class="section-head" data-i18n="basicHint">填写 Basic Authentication 信息,将作为 Authorization 请求头调用后台接口。</p>
632
- <div class="grid">
633
- <div class="field">
634
- <label for="basicUsername" data-i18n="basicUser">Basic 用户名</label>
635
- <input id="basicUsername" name="basicUsername" data-i18n-placeholder="basicUserPh">
636
- </div>
637
- <div class="field">
638
- <label for="basicPassword" data-i18n="basicPass">Basic 密码</label>
639
- <input id="basicPassword" name="basicPassword" type="password" data-i18n-placeholder="basicPassPh">
640
- </div>
641
- </div>
642
- </section>
643
-
644
- <section class="section" data-section="x-token">
645
- <p class="section-head" data-i18n="tokenHint">直接填写管理员登录后的 X-Token,用于后续所有后台接口调用。</p>
646
- <div class="field">
647
- <label for="xToken" data-i18n="xToken">X-Token</label>
648
- <input id="xToken" name="xToken" type="password" data-i18n-placeholder="xTokenPh">
649
- </div>
650
- </section>
651
-
652
- <section class="section" data-section="register-email">
653
- <p class="section-head" data-i18n="registerHint">使用邮箱注册开通新后台账号。需要验证码时,请先通过 SDK 获取邮箱验证码。</p>
654
- <div class="grid">
655
- <div class="field">
656
- <label for="registerEmail" data-i18n="email">邮箱地址</label>
657
- <input id="registerEmail" name="registerEmail" type="email" data-i18n-placeholder="emailPh">
658
- </div>
659
- <div class="field">
660
- <label for="registerPassword" data-i18n="newPassword">登录密码</label>
661
- <input id="registerPassword" name="registerPassword" type="password" data-i18n-placeholder="newPasswordPh">
662
- </div>
663
- <div class="field">
664
- <label for="registerName" data-i18n="name">姓名或昵称</label>
665
- <input id="registerName" name="registerName" data-i18n-placeholder="namePh">
666
- </div>
667
- <div class="field">
668
- <label for="mailCode" data-i18n="mailCode">邮箱验证码</label>
669
- <input id="mailCode" name="mailCode" data-i18n-placeholder="mailCodePh">
670
- </div>
671
- </div>
672
- </section>
673
-
674
- <button class="submit" type="submit" data-i18n="submit">完成授权</button>
802
+ ${renderSections()}
803
+
804
+ <button class="submit" type="submit" data-i18n="submit"></button>
675
805
  </form>
676
- <div class="security" data-i18n="security">请不要把密码、Token 或 Basic Authentication 内容粘贴到聊天窗口。</div>
677
- <p class="note" data-i18n="closeNote">提交成功后可以关闭本页面。</p>
806
+ <div class="security" data-i18n="security"></div>
807
+ <p class="note" data-i18n="closeNote"></p>
678
808
  </div>
679
809
  </section>
680
810
  </main>
681
811
  <script>
682
812
  const messages = ${JSON.stringify(I18N)}
683
813
  const initialType = ${JSON.stringify(initialType)}
814
+ const methodGroups = {
815
+ 'username-password': 'login',
816
+ 'email-password': 'login',
817
+ basic: 'login',
818
+ 'x-token': 'login',
819
+ 'register-email': 'register',
820
+ 'register-mobile': 'register'
821
+ }
822
+ const optionalFields = new Set(${JSON.stringify([...OPTIONAL_FIELDS])})
684
823
  let activeLang = localStorage.getItem('apifm-auth-lang') || 'zh-CN'
685
824
 
686
825
  const authTypeInput = document.querySelector('#authType')
687
826
  const sections = [...document.querySelectorAll('[data-section]')]
688
827
  const methodButtons = [...document.querySelectorAll('[data-method]')]
828
+ const groupTabs = [...document.querySelectorAll('[data-group-tab]')]
829
+ const methodGroupsEl = [...document.querySelectorAll('.method[data-group]')]
689
830
  const langButtons = [...document.querySelectorAll('[data-lang]')]
690
831
 
691
832
  function t(key) {
@@ -707,14 +848,32 @@ function renderForm({ authType, alias, expiresAt }) {
707
848
  })
708
849
  }
709
850
 
851
+ function setGroup(group) {
852
+ groupTabs.forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.groupTab === group)))
853
+ methodGroupsEl.forEach((node) => {
854
+ node.hidden = node.dataset.group !== group
855
+ })
856
+ const currentMethod = authTypeInput.value
857
+ if (methodGroups[currentMethod] !== group) {
858
+ const firstMethod = methodButtons.find((button) => methodGroups[button.dataset.method] === group)?.dataset.method
859
+ setMethod(firstMethod)
860
+ }
861
+ }
862
+
710
863
  function setMethod(method) {
864
+ if (!method) return
711
865
  authTypeInput.value = method
866
+ const group = methodGroups[method]
867
+ groupTabs.forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.groupTab === group)))
868
+ methodGroupsEl.forEach((node) => {
869
+ node.hidden = node.dataset.group !== group
870
+ })
712
871
  sections.forEach((section) => {
713
872
  const active = section.dataset.section === method
714
873
  section.classList.toggle('active', active)
715
874
  section.querySelectorAll('input').forEach((input) => {
716
875
  input.disabled = !active
717
- input.required = active && input.type !== 'hidden' && !['pdomain', 'registerName', 'mailCode'].includes(input.name)
876
+ input.required = active && !optionalFields.has(input.name)
718
877
  })
719
878
  })
720
879
  methodButtons.forEach((button) => {
@@ -723,6 +882,7 @@ function renderForm({ authType, alias, expiresAt }) {
723
882
  }
724
883
 
725
884
  methodButtons.forEach((button) => button.addEventListener('click', () => setMethod(button.dataset.method)))
885
+ groupTabs.forEach((button) => button.addEventListener('click', () => setGroup(button.dataset.groupTab)))
726
886
  langButtons.forEach((button) => button.addEventListener('click', () => setLang(button.dataset.lang)))
727
887
  setLang(activeLang)
728
888
  setMethod(initialType)
@@ -731,13 +891,84 @@ function renderForm({ authType, alias, expiresAt }) {
731
891
  </html>`
732
892
  }
733
893
 
894
+ function renderSections() {
895
+ return `
896
+ <section class="section" data-section="username-password">
897
+ <p class="section-head" data-i18n="usernameHint"></p>
898
+ <div class="grid">
899
+ ${field('loginUsername', 'loginUsername', 'username', 'usernamePh')}
900
+ ${field('loginPasswordUser', 'loginPassword', 'password', 'passwordPh', 'password')}
901
+ ${field('pdomain', 'pdomain', 'pdomain', 'pdomainPh')}
902
+ ${field('imgcode', 'imgcode', 'imgcode', 'imgcodePh')}
903
+ ${field('k', 'k', 'k', 'kPh')}
904
+ </div>
905
+ </section>
906
+
907
+ <section class="section" data-section="email-password">
908
+ <p class="section-head" data-i18n="emailHint"></p>
909
+ <div class="grid">
910
+ ${field('loginEmail', 'loginEmail', 'email', 'emailPh', 'email')}
911
+ ${field('loginPasswordEmail', 'loginPassword', 'password', 'passwordPh', 'password')}
912
+ ${field('imgcodeEmail', 'imgcode', 'imgcode', 'imgcodePh')}
913
+ ${field('kEmail', 'k', 'k', 'kPh')}
914
+ </div>
915
+ </section>
916
+
917
+ <section class="section" data-section="basic">
918
+ <p class="section-head" data-i18n="basicHint"></p>
919
+ <div class="grid">
920
+ ${field('basicUsername', 'basicUsername', 'merchantNo', 'merchantNoPh')}
921
+ ${field('basicPassword', 'basicPassword', 'merchantKey', 'merchantKeyPh', 'password')}
922
+ </div>
923
+ </section>
924
+
925
+ <section class="section" data-section="x-token">
926
+ <p class="section-head" data-i18n="tokenHint"></p>
927
+ ${field('xToken', 'xToken', 'xToken', 'xTokenPh', 'password')}
928
+ </section>
929
+
930
+ <section class="section" data-section="register-email">
931
+ <p class="section-head" data-i18n="registerEmailHint"></p>
932
+ <div class="grid">
933
+ ${field('registerEmail', 'registerEmail', 'email', 'emailPh', 'email')}
934
+ ${field('registerPasswordEmail', 'registerPassword', 'registerPassword', 'registerPasswordPh', 'password')}
935
+ ${field('registerNameEmail', 'registerName', 'name', 'namePh')}
936
+ ${field('mailCode', 'mailCode', 'mailCode', 'mailCodePh')}
937
+ ${field('referrerEmail', 'referrer', 'referrer', 'referrerPh')}
938
+ </div>
939
+ </section>
940
+
941
+ <section class="section" data-section="register-mobile">
942
+ <p class="section-head" data-i18n="registerMobileHint"></p>
943
+ <div class="grid">
944
+ ${field('registerMobile', 'registerMobile', 'mobile', 'mobilePh', 'tel')}
945
+ ${field('registerPasswordMobile', 'registerPassword', 'registerPassword', 'registerPasswordPh', 'password')}
946
+ ${field('registerNameMobile', 'registerName', 'name', 'namePh')}
947
+ ${field('smsCode', 'smsCode', 'smsCode', 'smsCodePh')}
948
+ ${field('registerType', 'registerType', 'type', 'typePh')}
949
+ ${field('referrerMobile', 'referrer', 'referrer', 'referrerPh')}
950
+ ${field('agentKey', 'agentKey', 'agentKey', 'agentKeyPh')}
951
+ ${field('smsImgCode', 'smsImgCode', 'imgcode', 'imgcodePh')}
952
+ ${field('smsK', 'smsK', 'k', 'kPh')}
953
+ </div>
954
+ </section>`
955
+ }
956
+
957
+ function field(id, name, labelKey, placeholderKey, type = 'text') {
958
+ return `<div class="field">
959
+ <label for="${id}" data-i18n="${labelKey}"></label>
960
+ <input id="${id}" name="${name}" type="${type}" data-i18n-placeholder="${placeholderKey}">
961
+ </div>`
962
+ }
963
+
734
964
  function renderMethodButton(method, activeType) {
735
965
  const keys = {
736
966
  'username-password': ['methodUsername', 'methodUsernameSub'],
737
967
  'email-password': ['methodEmail', 'methodEmailSub'],
738
968
  basic: ['methodBasic', 'methodBasicSub'],
739
969
  'x-token': ['methodToken', 'methodTokenSub'],
740
- 'register-email': ['methodRegister', 'methodRegisterSub']
970
+ 'register-email': ['methodRegisterEmail', 'methodRegisterEmailSub'],
971
+ 'register-mobile': ['methodRegisterMobile', 'methodRegisterMobileSub']
741
972
  }[method]
742
973
  return `<button type="button" role="tab" data-method="${method}" aria-pressed="${method === activeType}">
743
974
  <strong data-i18n="${keys[0]}"></strong>
@@ -750,5 +981,5 @@ function renderSuccess(account) {
750
981
  }
751
982
 
752
983
  function renderError(error) {
753
- return `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><body style="font:16px system-ui;padding:40px"><h1>Authorization failed</h1><p>${escapeHtml(error.message)}</p><p>Go back, check the values, and submit again.</p></body></html>`
984
+ return `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><body style="font:16px system-ui;padding:40px;background:#f7f8fb;color:#202124"><main style="max-width:620px;margin:10vh auto;background:white;border:1px solid #dde2ea;border-radius:14px;padding:32px"><h1>授权失败</h1><p>${escapeHtml(error.message)}</p><p>请返回上一页检查填写内容后重新提交。</p></main></body></html>`
754
985
  }
package/src/index.js CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  summarizeMetadata
22
22
  } from './sdk.js'
23
23
 
24
- const VERSION = '26.5.3'
24
+ const VERSION = '26.5.4'
25
25
 
26
26
  const server = new McpServer(
27
27
  {
@@ -44,13 +44,13 @@ server.registerTool(
44
44
  {
45
45
  title: 'Start secure APIFM admin authorization',
46
46
  description:
47
- 'Creates a localhost browser page for entering Basic Auth, X-Token, username/password, or email registration details. Secrets are stored only in this MCP process memory and must not be pasted into chat.',
47
+ 'Creates a localhost browser page for login, Basic Auth, X-Token, email registration, or mobile registration. Secrets are stored only in this MCP process memory and must not be pasted into chat.',
48
48
  inputSchema: z.object({
49
49
  authType: z
50
- .enum(['all', 'basic', 'x-token', 'password', 'username-password', 'email-password', 'register-email'])
50
+ .enum(['all', 'basic', 'x-token', 'password', 'username-password', 'email-password', 'register-email', 'register-mobile'])
51
51
  .optional()
52
52
  .describe(
53
- 'Optional initial auth method. all shows every method in one browser page. basic = Basic Authentication, x-token = existing admin X-Token, username-password/email-password = SDK login, register-email = create admin account by email.'
53
+ 'Optional initial auth method. all shows every method in one browser page. basic = Basic Authentication, x-token = existing admin X-Token, username-password/email-password = SDK login, register-email/register-mobile = create admin account.'
54
54
  ),
55
55
  alias: z.string().optional().describe('Friendly local account alias, for example prod or test-shop.'),
56
56
  domains: z
package/src/self-check.js CHANGED
@@ -41,12 +41,33 @@ try {
41
41
  throw new Error('start_auth should return a local authorization URL.')
42
42
  }
43
43
  const authPage = await fetch(authData.url).then((response) => response.text())
44
- for (const expected of ['data-lang="zh-CN"', 'data-lang="zh-TW"', 'data-lang="en"', 'data-method="basic"', 'data-method="x-token"']) {
44
+ for (const expected of [
45
+ 'data-lang="zh-CN"',
46
+ 'data-lang="zh-TW"',
47
+ 'data-lang="en"',
48
+ 'data-group-tab="login"',
49
+ 'data-group-tab="register"',
50
+ 'data-method="basic"',
51
+ 'data-method="x-token"',
52
+ 'data-method="register-mobile"',
53
+ 'merchantNo',
54
+ 'merchantKey'
55
+ ]) {
45
56
  if (!authPage.includes(expected)) {
46
57
  throw new Error(`Authorization page is missing ${expected}.`)
47
58
  }
48
59
  }
49
60
 
61
+ const mobileAuth = await client.callTool({
62
+ name: 'apifm_admin_start_auth',
63
+ arguments: { authType: 'register-mobile' }
64
+ })
65
+ const mobileAuthData = JSON.parse(mobileAuth.content[0].text)
66
+ const mobileAuthPage = await fetch(mobileAuthData.url).then((response) => response.text())
67
+ if (!mobileAuthPage.includes('value="register-mobile"')) {
68
+ throw new Error('register-mobile should be accepted as an initial auth type.')
69
+ }
70
+
50
71
  const search = await client.callTool({
51
72
  name: 'apifm_admin_search_methods',
52
73
  arguments: { query: 'user list', limit: 5 }