apifm-admin-mcp 26.5.4 → 26.5.5

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
@@ -1,32 +1,26 @@
1
1
  # apifm-admin-mcp
2
2
 
3
- `apifm-admin-mcp` is a stdio MCP server for AI agents that need to operate APIFM admin APIs through the [`apifm-admin`](https://www.npmjs.com/package/apifm-admin) SDK.
3
+ `apifm-admin-mcp` 是一个通过 `apifm-admin` SDK 调用 APIFM 后台接口的 MCP Server,可用于 Kiro、Cursor、Claude Code、Codex、Windsurf、Trae、Qoder 等支持 MCP Agent。
4
4
 
5
- It is designed for Kiro, Cursor, Claude Code, Codex, Windsurf, Trae, Qoder, and other MCP-compatible agents.
5
+ ## 安全原则
6
6
 
7
- ## Security Model
7
+ 密码、商户秘钥、X-Token 等敏感信息不要粘贴到聊天窗口。
8
8
 
9
- Secrets must not be pasted into model chat.
9
+ MCP 通过 `apifm_admin_start_auth` 返回一个本机 `127.0.0.1` 授权页面。用户在浏览器里填写敏感信息,信息只保存在当前 MCP 进程内存中,不会作为工具参数发送给大模型。
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, 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.
11
+ 如果尚未授权就调用后台 API,`apifm_admin_call` `apifm_admin_find_and_call` 会直接返回 `authUrl`,Agent 应该立即把这个 URL 展示给用户打开。
12
12
 
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
-
15
- The API callers reject payloads and headers containing sensitive field names such as `pwd`, `password`, `token`, `x-token`, `authorization`, `secret`, or `key`.
16
-
17
- Accounts are in-memory only. Restarting the MCP server clears all tokens and credentials.
18
-
19
- ## Install
13
+ ## 安装
20
14
 
21
15
  ```bash
22
16
  npm install -g apifm-admin-mcp
23
17
  ```
24
18
 
25
- The package depends on `apifm-admin` as a normal npm dependency. It does not bundle SDK source code, so newer compatible `apifm-admin` releases can add methods without requiring this MCP package to be regenerated.
19
+ 本包依赖 `apifm-admin`,但不会把 SDK 源码打包进 MCP。`apifm-admin` 后续升级新增的方法,会在运行时动态发现。
26
20
 
27
- ## MCP Configuration
21
+ ## MCP 配置
28
22
 
29
- Use stdio:
23
+ 使用 `npx`:
30
24
 
31
25
  ```json
32
26
  {
@@ -39,7 +33,7 @@ Use stdio:
39
33
  }
40
34
  ```
41
35
 
42
- If installed globally:
36
+ 全局安装后:
43
37
 
44
38
  ```json
45
39
  {
@@ -51,28 +45,43 @@ If installed globally:
51
45
  }
52
46
  ```
53
47
 
54
- ## Tools
48
+ ## 授权方式
49
+
50
+ 授权页面分为“登录现有账号”和“注册新账号”。
51
+
52
+ 登录现有账号:
53
+
54
+ - 手机号登录:调用 `loginAdminMobile`,也就是“手机号码登录”方法,成功后取返回的 X-Token 作为后续 API 凭证。
55
+ - 邮箱登录:调用 `loginAdminEmail`,也就是“邮箱登录获取X-TOKEN”方法,成功后取返回的 X-Token 作为后续 API 凭证。
56
+ - 直接填写 X-Token:不调用登录接口,直接把填写的 X-Token 作为后续 API 凭证。
57
+ - Basic Authentication:填写商户号和商户秘钥,后续请求同时写入 `Authentication` 和 `Authorization` 请求头。
58
+
59
+ 注册新账号:
60
+
61
+ - 邮箱注册:调用邮箱注册保存接口,成功后尝试邮箱登录获取 X-Token。
62
+ - 手机号注册:调用手机号注册保存接口,成功后尝试手机号登录获取 X-Token。
55
63
 
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
- - `apifm_admin_accounts`: Lists local account aliases without revealing secrets.
58
- - `apifm_admin_switch_account`: Switches the active account alias.
59
- - `apifm_admin_remove_account`: Clears an account alias and its in-memory secrets.
60
- - `apifm_admin_find_and_call`: Searches for the best matching SDK method, calls it, and returns the live backend response in `apiResult`.
61
- - `apifm_admin_call`: Calls an exact SDK method using the selected local account and returns the live backend response in `apiResult`.
62
- - `apifm_admin_search_methods`: Planning helper only. Searches methods dynamically from the installed `apifm-admin` SDK, but does not call the API.
63
- - `apifm_admin_method_info`: Planning helper only. Shows route, method, parameters, and usage for one SDK method, but does not call the API.
64
+ ## 工具
64
65
 
65
- For real backend data, agents should call `apifm_admin_find_and_call` or `apifm_admin_call`. The final API response is returned under `apiResult`.
66
+ - `apifm_admin_start_auth`:启动本机授权页面,支持简体中文、繁体中文、英文。
67
+ - `apifm_admin_accounts`:查看当前 MCP 进程内已授权账号,不返回密钥。
68
+ - `apifm_admin_switch_account`:切换当前使用的账号别名。
69
+ - `apifm_admin_remove_account`:删除当前 MCP 进程内保存的账号凭证。
70
+ - `apifm_admin_find_and_call`:按自然语言搜索 SDK 方法并调用,真实接口返回在 `apiResult`。
71
+ - `apifm_admin_call`:按指定 SDK 方法名调用后台 API,真实接口返回在 `apiResult`。
72
+ - `apifm_admin_search_methods`:只搜索 SDK 方法和参数说明,不会调用 API。
73
+ - `apifm_admin_method_info`:只查看某个 SDK 方法说明,不会调用 API。
66
74
 
67
- ## Example Agent Flow
75
+ ## 使用流程
68
76
 
69
- 1. User: "Log in to my admin account and read the user list."
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 a login method or a registration method.
72
- 4. User completes authorization in the local browser page.
73
- 5. Agent retries the same API call and answers from the returned `apiResult`, not from method documentation.
77
+ 1. 用户让 Agent 读取或操作后台数据。
78
+ 2. Agent 调用 `apifm_admin_find_and_call` `apifm_admin_call`。
79
+ 3. 如果还没授权,工具返回 `authUrl`。
80
+ 4. 用户打开 `authUrl`,选择手机号登录、邮箱登录、X-Token Basic Authentication。
81
+ 5. 授权成功后,Agent 重新调用刚才的 API
82
+ 6. Agent 使用 `apiResult` 中的真实接口返回回答用户。
74
83
 
75
- ## Local Check
84
+ ## 本地检查
76
85
 
77
86
  ```bash
78
87
  npm run check
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apifm-admin-mcp",
3
- "version": "26.5.4",
3
+ "version": "26.5.5",
4
4
  "description": "MCP server for safely operating APIFM admin APIs through the apifm-admin SDK.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,7 +10,8 @@ const AUTH_TYPES = new Set([
10
10
  'all',
11
11
  'basic',
12
12
  'x-token',
13
- 'username-password',
13
+ 'password',
14
+ 'mobile-password',
14
15
  'email-password',
15
16
  'register-email',
16
17
  'register-mobile'
@@ -18,7 +19,6 @@ const AUTH_TYPES = new Set([
18
19
 
19
20
  const OPTIONAL_FIELDS = new Set([
20
21
  'alias',
21
- 'pdomain',
22
22
  'imgcode',
23
23
  'k',
24
24
  'registerName',
@@ -26,9 +26,7 @@ const OPTIONAL_FIELDS = new Set([
26
26
  'referrer',
27
27
  'agentKey',
28
28
  'mailCode',
29
- 'smsCode',
30
- 'smsImgCode',
31
- 'smsK'
29
+ 'smsCode'
32
30
  ])
33
31
 
34
32
  const I18N = {
@@ -46,8 +44,8 @@ const I18N = {
46
44
  aliasPh: 'default-admin',
47
45
  tabLogin: '登录现有账号',
48
46
  tabRegister: '注册新账号',
49
- methodUsername: '用户名登录',
50
- methodUsernameSub: '用户名 + 密码',
47
+ methodMobile: '手机号登录',
48
+ methodMobileSub: '手机号码 + 密码',
51
49
  methodEmail: '邮箱登录',
52
50
  methodEmailSub: '邮箱 + 密码',
53
51
  methodBasic: 'Basic Auth',
@@ -58,18 +56,14 @@ const I18N = {
58
56
  methodRegisterEmailSub: '邮箱验证码',
59
57
  methodRegisterMobile: '手机号注册',
60
58
  methodRegisterMobileSub: '短信验证码',
61
- usernameHint: '使用后台用户名和密码登录,MCP 会通过 SDK 换取 X-Token。',
62
- emailHint: '使用后台邮箱和密码登录,MCP 会通过 SDK 换取 X-Token。',
63
- basicHint: '填写 Basic Authentication 信息,将作为 Authorization 请求头调用后台接口。',
64
- tokenHint: '直接填写管理员登录后的 X-Token,用于后续所有后台接口调用。',
65
- registerEmailHint: '使用邮箱注册开通新后台账号,提交成功后 MCP 会尝试自动登录。',
66
- registerMobileHint: '使用手机号注册开通新后台账号,提交成功后 MCP 会尝试自动登录。',
67
- username: '用户名',
68
- usernamePh: '请输入后台用户名',
59
+ mobileHint: '使用后台手机号码和密码登录,MCP 会调用 loginAdminMobile 手机号码登录方法获取 X-Token。',
60
+ emailHint: '使用后台邮箱和密码登录,MCP 会调用 loginAdminEmail 邮箱登录获取 X-TOKEN 方法。',
61
+ basicHint: '填写 Basic Authentication 信息,后续请求会写入 Authentication 和 Authorization 请求头。',
62
+ tokenHint: '直接填写管理员登录后的 X-Token,MCP 会把它作为后续调用后台 API 的凭证。',
63
+ registerEmailHint: '使用邮箱注册开通新后台账号,提交成功后 MCP 会尝试邮箱登录获取 X-Token。',
64
+ registerMobileHint: '使用手机号注册开通新后台账号,提交成功后 MCP 会尝试手机号登录获取 X-Token。',
69
65
  password: '密码',
70
66
  passwordPh: '请输入登录密码',
71
- pdomain: '专属域名,可选',
72
- pdomainPh: '例如 yourdomain',
73
67
  imgcode: '图形验证码,可选',
74
68
  imgcodePh: '请输入图形验证码',
75
69
  k: '验证码随机数,可选',
@@ -116,8 +110,8 @@ const I18N = {
116
110
  aliasPh: 'default-admin',
117
111
  tabLogin: '登入現有帳號',
118
112
  tabRegister: '註冊新帳號',
119
- methodUsername: '使用者名稱登入',
120
- methodUsernameSub: '使用者名稱 + 密碼',
113
+ methodMobile: '手機號登入',
114
+ methodMobileSub: '手機號碼 + 密碼',
121
115
  methodEmail: '郵箱登入',
122
116
  methodEmailSub: '郵箱 + 密碼',
123
117
  methodBasic: 'Basic Auth',
@@ -128,18 +122,14 @@ const I18N = {
128
122
  methodRegisterEmailSub: '郵箱驗證碼',
129
123
  methodRegisterMobile: '手機號註冊',
130
124
  methodRegisterMobileSub: '簡訊驗證碼',
131
- usernameHint: '使用後台使用者名稱和密碼登入,MCP 會透過 SDK 換取 X-Token。',
132
- emailHint: '使用後台郵箱和密碼登入,MCP 會透過 SDK 換取 X-Token。',
133
- basicHint: '填寫 Basic Authentication 資訊,將作為 Authorization 請求頭呼叫後台介面。',
134
- tokenHint: '直接填寫管理員登入後的 X-Token,用於後續所有後台介面呼叫。',
135
- registerEmailHint: '使用郵箱註冊開通新後台帳號,提交成功後 MCP 會嘗試自動登入。',
136
- registerMobileHint: '使用手機號註冊開通新後台帳號,提交成功後 MCP 會嘗試自動登入。',
137
- username: '使用者名稱',
138
- usernamePh: '請輸入後台使用者名稱',
125
+ mobileHint: '使用後台手機號碼和密碼登入,MCP 會呼叫 loginAdminMobile 手機號碼登入方法取得 X-Token。',
126
+ emailHint: '使用後台郵箱和密碼登入,MCP 會呼叫 loginAdminEmail 郵箱登入取得 X-TOKEN 方法。',
127
+ basicHint: '填寫 Basic Authentication 資訊,後續請求會寫入 Authentication 和 Authorization 請求頭。',
128
+ tokenHint: '直接填寫管理員登入後的 X-Token,MCP 會把它作為後續呼叫後台 API 的憑證。',
129
+ registerEmailHint: '使用郵箱註冊開通新後台帳號,提交成功後 MCP 會嘗試郵箱登入取得 X-Token。',
130
+ registerMobileHint: '使用手機號註冊開通新後台帳號,提交成功後 MCP 會嘗試手機號登入取得 X-Token。',
139
131
  password: '密碼',
140
132
  passwordPh: '請輸入登入密碼',
141
- pdomain: '專屬網域,可選',
142
- pdomainPh: '例如 yourdomain',
143
133
  imgcode: '圖形驗證碼,可選',
144
134
  imgcodePh: '請輸入圖形驗證碼',
145
135
  k: '驗證碼隨機數,可選',
@@ -186,8 +176,8 @@ const I18N = {
186
176
  aliasPh: 'default-admin',
187
177
  tabLogin: 'Log in',
188
178
  tabRegister: 'Register',
189
- methodUsername: 'Username login',
190
- methodUsernameSub: 'Username + password',
179
+ methodMobile: 'Mobile login',
180
+ methodMobileSub: 'Mobile + password',
191
181
  methodEmail: 'Email login',
192
182
  methodEmailSub: 'Email + password',
193
183
  methodBasic: 'Basic Auth',
@@ -198,18 +188,14 @@ const I18N = {
198
188
  methodRegisterEmailSub: 'Email code',
199
189
  methodRegisterMobile: 'Mobile signup',
200
190
  methodRegisterMobileSub: 'SMS code',
201
- usernameHint: 'Log in with an admin username and password. The MCP will exchange them for an X-Token through the SDK.',
202
- emailHint: 'Log in with an admin email and password. The MCP will exchange them for an X-Token through the SDK.',
203
- basicHint: 'Enter Basic Authentication credentials. They will be sent as the Authorization header for API calls.',
204
- tokenHint: 'Enter an existing admin X-Token for later backend API calls.',
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.',
207
- username: 'Username',
208
- usernamePh: 'Enter admin username',
191
+ mobileHint: 'Log in with an admin mobile number and password. The MCP calls the mobile login SDK method to get X-Token.',
192
+ emailHint: 'Log in with an admin email and password. The MCP calls the email login SDK method to get X-Token.',
193
+ basicHint: 'Enter Basic Authentication credentials. API calls will include both Authentication and Authorization headers.',
194
+ tokenHint: 'Enter an existing admin X-Token. The MCP will use it as the credential for later admin API calls.',
195
+ registerEmailHint: 'Create a new admin account by email. After signup, the MCP will try email login to get X-Token.',
196
+ registerMobileHint: 'Create a new admin account by mobile. After signup, the MCP will try mobile login to get X-Token.',
209
197
  password: 'Password',
210
198
  passwordPh: 'Enter login password',
211
- pdomain: 'Private domain, optional',
212
- pdomainPh: 'Example: yourdomain',
213
199
  imgcode: 'Image code, optional',
214
200
  imgcodePh: 'Enter image code',
215
201
  k: 'Captcha nonce, optional',
@@ -327,7 +313,7 @@ async function finishAuth({ authType, alias, domains, fields }) {
327
313
  })
328
314
  }
329
315
 
330
- if (selectedAuthType === 'username-password' || selectedAuthType === 'email-password') {
316
+ if (selectedAuthType === 'mobile-password' || selectedAuthType === 'email-password') {
331
317
  const token = await loginAndExtractToken({ authType: selectedAuthType, domains, fields })
332
318
  return upsertAccount({
333
319
  alias: fields.alias || alias,
@@ -386,17 +372,6 @@ async function finishAuth({ authType, alias, domains, fields }) {
386
372
 
387
373
  async function loginAndExtractToken({ authType, domains, fields }) {
388
374
  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
375
  'email-password': {
401
376
  methodName: 'loginAdminEmail',
402
377
  payload: {
@@ -410,11 +385,11 @@ async function loginAndExtractToken({ authType, domains, fields }) {
410
385
  'mobile-password': {
411
386
  methodName: 'loginAdminMobile',
412
387
  payload: {
413
- mobile: required(fields.registerMobile, 'mobile'),
414
- pwd: required(fields.registerPassword, 'password'),
388
+ mobile: required(fields.loginMobile || fields.registerMobile, 'mobile'),
389
+ pwd: required(fields.loginPassword || fields.registerPassword, 'password'),
415
390
  rememberMe: true,
416
- imgcode: fields.smsImgCode || fields.imgcode || undefined,
417
- k: fields.smsK || fields.k || undefined
391
+ imgcode: fields.imgcode || undefined,
392
+ k: fields.k || undefined
418
393
  }
419
394
  }
420
395
  }[authType]
@@ -450,7 +425,7 @@ function compactObject(input) {
450
425
  }
451
426
 
452
427
  function normalizeAuthType(authType) {
453
- if (authType === 'password') return 'username-password'
428
+ if (authType === 'password' || authType === 'username-password') return 'mobile-password'
454
429
  if (AUTH_TYPES.has(authType)) return authType
455
430
  return 'all'
456
431
  }
@@ -478,8 +453,7 @@ function extractToken(response) {
478
453
  if (typeof value === 'string' && value.trim()) return value.trim()
479
454
  }
480
455
 
481
- const found = findTokenDeep(response)
482
- return found || ''
456
+ return findTokenDeep(response) || ''
483
457
  }
484
458
 
485
459
  function getPath(value, path) {
@@ -500,7 +474,7 @@ function findTokenDeep(value, depth = 0) {
500
474
  if (
501
475
  typeof child === 'string' &&
502
476
  child.trim() &&
503
- ['token', 'xtoken', 'xToken', 'xstoken', 'admintoken', 'logintoken'].map((item) => item.toLowerCase()).includes(compactKey)
477
+ ['token', 'xtoken', 'xstoken', 'admintoken', 'logintoken'].includes(compactKey)
504
478
  ) {
505
479
  return child.trim()
506
480
  }
@@ -563,7 +537,7 @@ function escapeHtml(value) {
563
537
  }
564
538
 
565
539
  function renderForm({ authType, alias, expiresAt }) {
566
- const initialType = authType === 'all' ? 'username-password' : authType
540
+ const initialType = authType === 'all' ? 'mobile-password' : authType
567
541
  const initialGroup = initialType.startsWith('register-') ? 'register' : 'login'
568
542
  return `<!doctype html>
569
543
  <html lang="zh-CN">
@@ -583,7 +557,6 @@ function renderForm({ authType, alias, expiresAt }) {
583
557
  --accent: oklch(0.56 0.19 255);
584
558
  --accent-ink: oklch(1 0 0);
585
559
  --accent-soft: oklch(0.94 0.035 255);
586
- --success: oklch(0.58 0.15 155);
587
560
  }
588
561
  * { box-sizing: border-box; }
589
562
  body {
@@ -595,44 +568,11 @@ function renderForm({ authType, alias, expiresAt }) {
595
568
  linear-gradient(135deg, var(--bg), oklch(0.955 0.011 230));
596
569
  color: var(--ink);
597
570
  }
598
- .shell {
599
- width: min(1120px, calc(100vw - 32px));
600
- min-height: 100vh;
601
- margin: 0 auto;
602
- display: grid;
603
- place-items: center;
604
- padding: 28px 0;
605
- }
606
- .panel {
607
- width: 100%;
608
- display: grid;
609
- grid-template-columns: minmax(280px, 0.78fr) minmax(340px, 1.22fr);
610
- background: var(--surface);
611
- border: 1px solid var(--line);
612
- border-radius: 16px;
613
- overflow: hidden;
614
- box-shadow: 0 8px 28px oklch(0.25 0.02 255 / 0.12);
615
- }
616
- .aside {
617
- padding: 34px;
618
- background: linear-gradient(180deg, oklch(0.26 0.07 255), oklch(0.19 0.04 255));
619
- color: white;
620
- display: flex;
621
- flex-direction: column;
622
- justify-content: space-between;
623
- gap: 32px;
624
- }
571
+ .shell { width: min(1120px, calc(100vw - 32px)); min-height: 100vh; margin: 0 auto; display: grid; place-items: center; padding: 28px 0; }
572
+ .panel { width: 100%; display: grid; grid-template-columns: minmax(280px, 0.78fr) minmax(340px, 1.22fr); background: var(--surface); border: 1px solid var(--line); border-radius: 16px; overflow: hidden; box-shadow: 0 8px 28px oklch(0.25 0.02 255 / 0.12); }
573
+ .aside { padding: 34px; background: linear-gradient(180deg, oklch(0.26 0.07 255), oklch(0.19 0.04 255)); color: white; display: flex; flex-direction: column; justify-content: space-between; gap: 32px; }
625
574
  .brand { display: flex; align-items: center; gap: 12px; font-weight: 760; }
626
- .mark {
627
- width: 38px;
628
- height: 38px;
629
- display: grid;
630
- place-items: center;
631
- border-radius: 10px;
632
- background: oklch(0.74 0.16 210);
633
- color: oklch(0.18 0.04 255);
634
- font-weight: 850;
635
- }
575
+ .mark { width: 38px; height: 38px; display: grid; place-items: center; border-radius: 10px; background: oklch(0.74 0.16 210); color: oklch(0.18 0.04 255); font-weight: 850; }
636
576
  h1 { margin: 28px 0 12px; font-size: 2rem; line-height: 1.12; text-wrap: balance; letter-spacing: 0; }
637
577
  .lead { margin: 0; color: oklch(0.88 0.018 250); max-width: 48ch; }
638
578
  .trust { display: grid; gap: 10px; margin: 28px 0 0; padding: 0; list-style: none; }
@@ -641,41 +581,14 @@ function renderForm({ authType, alias, expiresAt }) {
641
581
  .content { padding: 30px; }
642
582
  .topbar { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom: 20px; }
643
583
  .meta { color: var(--muted); font-size: 0.92rem; }
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 {
654
- appearance: none;
655
- border: 0;
656
- font: inherit;
657
- cursor: pointer;
658
- }
584
+ .lang, .tabs { display: inline-grid; grid-auto-flow: column; gap: 4px; padding: 4px; background: var(--surface-2); border: 1px solid var(--line); border-radius: 999px; }
585
+ .lang button, .tabs button, .method button { appearance: none; border: 0; font: inherit; cursor: pointer; }
659
586
  .lang button, .tabs button { padding: 7px 12px; border-radius: 999px; color: var(--muted); background: transparent; }
660
587
  .lang button[aria-pressed="true"], .tabs button[aria-pressed="true"] { background: var(--ink); color: white; }
661
588
  .tabs { margin-bottom: 14px; }
662
- .method {
663
- display: grid;
664
- grid-template-columns: repeat(4, minmax(0, 1fr));
665
- gap: 8px;
666
- margin-bottom: 22px;
667
- }
589
+ .method { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; margin-bottom: 22px; }
668
590
  .method[data-group="register"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
669
- .method button {
670
- min-height: 62px;
671
- padding: 10px;
672
- border: 1px solid var(--line);
673
- border-radius: 10px;
674
- background: var(--surface-2);
675
- color: var(--ink);
676
- text-align: left;
677
- transition: border-color 160ms ease, background 160ms ease, transform 160ms ease;
678
- }
591
+ .method button { min-height: 62px; padding: 10px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-2); color: var(--ink); text-align: left; transition: border-color 160ms ease, background 160ms ease, transform 160ms ease; }
679
592
  .method button:hover { border-color: oklch(0.72 0.04 255); transform: translateY(-1px); }
680
593
  .method button[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); }
681
594
  .method strong { display: block; font-size: 0.92rem; line-height: 1.2; }
@@ -684,67 +597,20 @@ function renderForm({ authType, alias, expiresAt }) {
684
597
  .grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
685
598
  .field { display: grid; gap: 7px; }
686
599
  label { color: var(--ink); font-weight: 660; }
687
- input {
688
- width: 100%;
689
- min-height: 44px;
690
- padding: 10px 12px;
691
- border: 1px solid var(--line);
692
- border-radius: 8px;
693
- background: white;
694
- color: var(--ink);
695
- font: inherit;
696
- outline: none;
697
- transition: border-color 160ms ease, box-shadow 160ms ease;
698
- }
600
+ input { width: 100%; min-height: 44px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 8px; background: white; color: var(--ink); font: inherit; outline: none; transition: border-color 160ms ease, box-shadow 160ms ease; }
699
601
  input::placeholder { color: oklch(0.48 0.025 255); }
700
602
  input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px oklch(0.62 0.16 255 / 0.18); }
701
- .section {
702
- display: none;
703
- padding: 18px;
704
- border: 1px solid var(--line);
705
- border-radius: 12px;
706
- background: var(--surface-2);
707
- }
603
+ .section { display: none; padding: 18px; border: 1px solid var(--line); border-radius: 12px; background: var(--surface-2); }
708
604
  .section.active { display: block; }
709
605
  .section-head { margin: 0 0 14px; color: var(--muted); }
710
- .submit {
711
- min-height: 46px;
712
- border: 0;
713
- border-radius: 8px;
714
- background: var(--accent);
715
- color: var(--accent-ink);
716
- font: inherit;
717
- font-weight: 760;
718
- cursor: pointer;
719
- transition: transform 160ms ease, filter 160ms ease;
720
- }
606
+ .submit { min-height: 46px; border: 0; border-radius: 8px; background: var(--accent); color: var(--accent-ink); font: inherit; font-weight: 760; cursor: pointer; transition: transform 160ms ease, filter 160ms ease; }
721
607
  .submit:hover { filter: brightness(1.03); transform: translateY(-1px); }
722
608
  .note { margin: 12px 0 0; color: var(--muted); font-size: 0.9rem; }
723
- .security {
724
- margin-top: 20px;
725
- padding: 12px 14px;
726
- border-radius: 10px;
727
- background: oklch(0.96 0.025 155);
728
- color: oklch(0.31 0.07 155);
729
- font-size: 0.9rem;
730
- }
609
+ .security { margin-top: 20px; padding: 12px 14px; border-radius: 10px; background: oklch(0.96 0.025 155); color: oklch(0.31 0.07 155); font-size: 0.9rem; }
731
610
  [hidden] { display: none !important; }
732
- @media (max-width: 900px) {
733
- .panel { grid-template-columns: 1fr; }
734
- .aside { padding: 26px; }
735
- .method { grid-template-columns: repeat(2, minmax(0, 1fr)); }
736
- .grid { grid-template-columns: 1fr; }
737
- }
738
- @media (max-width: 520px) {
739
- .shell { width: min(100vw - 20px, 1120px); padding: 10px 0; }
740
- .content { padding: 20px; }
741
- .topbar { align-items: flex-start; flex-direction: column; }
742
- .method, .method[data-group="register"] { grid-template-columns: 1fr; }
743
- h1 { font-size: 1.55rem; }
744
- }
745
- @media (prefers-reduced-motion: reduce) {
746
- *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
747
- }
611
+ @media (max-width: 900px) { .panel { grid-template-columns: 1fr; } .aside { padding: 26px; } .method { grid-template-columns: repeat(2, minmax(0, 1fr)); } .grid { grid-template-columns: 1fr; } }
612
+ @media (max-width: 520px) { .shell { width: min(100vw - 20px, 1120px); padding: 10px 0; } .content { padding: 20px; } .topbar { align-items: flex-start; flex-direction: column; } .method, .method[data-group="register"] { grid-template-columns: 1fr; } h1 { font-size: 1.55rem; } }
613
+ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; } }
748
614
  </style>
749
615
  </head>
750
616
  <body>
@@ -775,14 +641,12 @@ function renderForm({ authType, alias, expiresAt }) {
775
641
  <button type="button" data-lang="en" aria-pressed="false">EN</button>
776
642
  </div>
777
643
  </div>
778
-
779
644
  <div class="tabs" role="tablist" aria-label="Account mode">
780
645
  <button type="button" data-group-tab="login" aria-pressed="${initialGroup === 'login'}" data-i18n="tabLogin"></button>
781
646
  <button type="button" data-group-tab="register" aria-pressed="${initialGroup === 'register'}" data-i18n="tabRegister"></button>
782
647
  </div>
783
-
784
648
  <div class="method" data-group="login">
785
- ${renderMethodButton('username-password', initialType)}
649
+ ${renderMethodButton('mobile-password', initialType)}
786
650
  ${renderMethodButton('email-password', initialType)}
787
651
  ${renderMethodButton('basic', initialType)}
788
652
  ${renderMethodButton('x-token', initialType)}
@@ -791,16 +655,13 @@ function renderForm({ authType, alias, expiresAt }) {
791
655
  ${renderMethodButton('register-email', initialType)}
792
656
  ${renderMethodButton('register-mobile', initialType)}
793
657
  </div>
794
-
795
658
  <form method="post" autocomplete="off" id="authForm">
796
659
  <input type="hidden" id="authType" name="authType" value="${escapeHtml(initialType)}">
797
660
  <div class="field">
798
661
  <label for="alias" data-i18n="alias"></label>
799
662
  <input id="alias" name="alias" value="${escapeHtml(alias)}" data-i18n-placeholder="aliasPh">
800
663
  </div>
801
-
802
664
  ${renderSections()}
803
-
804
665
  <button class="submit" type="submit" data-i18n="submit"></button>
805
666
  </form>
806
667
  <div class="security" data-i18n="security"></div>
@@ -812,7 +673,7 @@ function renderForm({ authType, alias, expiresAt }) {
812
673
  const messages = ${JSON.stringify(I18N)}
813
674
  const initialType = ${JSON.stringify(initialType)}
814
675
  const methodGroups = {
815
- 'username-password': 'login',
676
+ 'mobile-password': 'login',
816
677
  'email-password': 'login',
817
678
  basic: 'login',
818
679
  'x-token': 'login',
@@ -821,53 +682,36 @@ function renderForm({ authType, alias, expiresAt }) {
821
682
  }
822
683
  const optionalFields = new Set(${JSON.stringify([...OPTIONAL_FIELDS])})
823
684
  let activeLang = localStorage.getItem('apifm-auth-lang') || 'zh-CN'
824
-
825
685
  const authTypeInput = document.querySelector('#authType')
826
686
  const sections = [...document.querySelectorAll('[data-section]')]
827
687
  const methodButtons = [...document.querySelectorAll('[data-method]')]
828
688
  const groupTabs = [...document.querySelectorAll('[data-group-tab]')]
829
689
  const methodGroupsEl = [...document.querySelectorAll('.method[data-group]')]
830
690
  const langButtons = [...document.querySelectorAll('[data-lang]')]
831
-
832
- function t(key) {
833
- return messages[activeLang][key] || messages['zh-CN'][key] || key
834
- }
835
-
691
+ function t(key) { return messages[activeLang][key] || messages['zh-CN'][key] || key }
836
692
  function setLang(lang) {
837
693
  activeLang = messages[lang] ? lang : 'zh-CN'
838
694
  localStorage.setItem('apifm-auth-lang', activeLang)
839
695
  document.documentElement.lang = activeLang
840
- document.querySelectorAll('[data-i18n]').forEach((node) => {
841
- node.textContent = t(node.dataset.i18n)
842
- })
843
- document.querySelectorAll('[data-i18n-placeholder]').forEach((node) => {
844
- node.placeholder = t(node.dataset.i18nPlaceholder)
845
- })
846
- langButtons.forEach((button) => {
847
- button.setAttribute('aria-pressed', String(button.dataset.lang === activeLang))
848
- })
696
+ document.querySelectorAll('[data-i18n]').forEach((node) => { node.textContent = t(node.dataset.i18n) })
697
+ document.querySelectorAll('[data-i18n-placeholder]').forEach((node) => { node.placeholder = t(node.dataset.i18nPlaceholder) })
698
+ langButtons.forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.lang === activeLang)))
849
699
  }
850
-
851
700
  function setGroup(group) {
852
701
  groupTabs.forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.groupTab === group)))
853
- methodGroupsEl.forEach((node) => {
854
- node.hidden = node.dataset.group !== group
855
- })
702
+ methodGroupsEl.forEach((node) => { node.hidden = node.dataset.group !== group })
856
703
  const currentMethod = authTypeInput.value
857
704
  if (methodGroups[currentMethod] !== group) {
858
705
  const firstMethod = methodButtons.find((button) => methodGroups[button.dataset.method] === group)?.dataset.method
859
706
  setMethod(firstMethod)
860
707
  }
861
708
  }
862
-
863
709
  function setMethod(method) {
864
710
  if (!method) return
865
711
  authTypeInput.value = method
866
712
  const group = methodGroups[method]
867
713
  groupTabs.forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.groupTab === group)))
868
- methodGroupsEl.forEach((node) => {
869
- node.hidden = node.dataset.group !== group
870
- })
714
+ methodGroupsEl.forEach((node) => { node.hidden = node.dataset.group !== group })
871
715
  sections.forEach((section) => {
872
716
  const active = section.dataset.section === method
873
717
  section.classList.toggle('active', active)
@@ -876,11 +720,8 @@ function renderForm({ authType, alias, expiresAt }) {
876
720
  input.required = active && !optionalFields.has(input.name)
877
721
  })
878
722
  })
879
- methodButtons.forEach((button) => {
880
- button.setAttribute('aria-pressed', String(button.dataset.method === method))
881
- })
723
+ methodButtons.forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.method === method)))
882
724
  }
883
-
884
725
  methodButtons.forEach((button) => button.addEventListener('click', () => setMethod(button.dataset.method)))
885
726
  groupTabs.forEach((button) => button.addEventListener('click', () => setGroup(button.dataset.groupTab)))
886
727
  langButtons.forEach((button) => button.addEventListener('click', () => setLang(button.dataset.lang)))
@@ -893,17 +734,15 @@ function renderForm({ authType, alias, expiresAt }) {
893
734
 
894
735
  function renderSections() {
895
736
  return `
896
- <section class="section" data-section="username-password">
897
- <p class="section-head" data-i18n="usernameHint"></p>
737
+ <section class="section" data-section="mobile-password">
738
+ <p class="section-head" data-i18n="mobileHint"></p>
898
739
  <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')}
740
+ ${field('loginMobile', 'loginMobile', 'mobile', 'mobilePh', 'tel')}
741
+ ${field('loginPasswordMobile', 'loginPassword', 'password', 'passwordPh', 'password')}
742
+ ${field('imgcodeMobile', 'imgcode', 'imgcode', 'imgcodePh')}
743
+ ${field('kMobile', 'k', 'k', 'kPh')}
904
744
  </div>
905
745
  </section>
906
-
907
746
  <section class="section" data-section="email-password">
908
747
  <p class="section-head" data-i18n="emailHint"></p>
909
748
  <div class="grid">
@@ -913,7 +752,6 @@ function renderSections() {
913
752
  ${field('kEmail', 'k', 'k', 'kPh')}
914
753
  </div>
915
754
  </section>
916
-
917
755
  <section class="section" data-section="basic">
918
756
  <p class="section-head" data-i18n="basicHint"></p>
919
757
  <div class="grid">
@@ -921,12 +759,10 @@ function renderSections() {
921
759
  ${field('basicPassword', 'basicPassword', 'merchantKey', 'merchantKeyPh', 'password')}
922
760
  </div>
923
761
  </section>
924
-
925
762
  <section class="section" data-section="x-token">
926
763
  <p class="section-head" data-i18n="tokenHint"></p>
927
764
  ${field('xToken', 'xToken', 'xToken', 'xTokenPh', 'password')}
928
765
  </section>
929
-
930
766
  <section class="section" data-section="register-email">
931
767
  <p class="section-head" data-i18n="registerEmailHint"></p>
932
768
  <div class="grid">
@@ -937,7 +773,6 @@ function renderSections() {
937
773
  ${field('referrerEmail', 'referrer', 'referrer', 'referrerPh')}
938
774
  </div>
939
775
  </section>
940
-
941
776
  <section class="section" data-section="register-mobile">
942
777
  <p class="section-head" data-i18n="registerMobileHint"></p>
943
778
  <div class="grid">
@@ -948,8 +783,6 @@ function renderSections() {
948
783
  ${field('registerType', 'registerType', 'type', 'typePh')}
949
784
  ${field('referrerMobile', 'referrer', 'referrer', 'referrerPh')}
950
785
  ${field('agentKey', 'agentKey', 'agentKey', 'agentKeyPh')}
951
- ${field('smsImgCode', 'smsImgCode', 'imgcode', 'imgcodePh')}
952
- ${field('smsK', 'smsK', 'k', 'kPh')}
953
786
  </div>
954
787
  </section>`
955
788
  }
@@ -963,7 +796,7 @@ function field(id, name, labelKey, placeholderKey, type = 'text') {
963
796
 
964
797
  function renderMethodButton(method, activeType) {
965
798
  const keys = {
966
- 'username-password': ['methodUsername', 'methodUsernameSub'],
799
+ 'mobile-password': ['methodMobile', 'methodMobileSub'],
967
800
  'email-password': ['methodEmail', 'methodEmailSub'],
968
801
  basic: ['methodBasic', 'methodBasicSub'],
969
802
  'x-token': ['methodToken', 'methodTokenSub'],
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.4'
24
+ const VERSION = '26.5.5'
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 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.',
47
+ 'Creates a localhost browser page for mobile login, email 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', 'register-mobile'])
50
+ .enum(['all', 'basic', 'x-token', 'password', 'mobile-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/register-mobile = create admin account.'
53
+ 'Optional initial auth method. all shows every method in one browser page. basic = Basic Authentication, x-token = existing admin X-Token, mobile-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
@@ -304,6 +304,7 @@ async function callToolResult({
304
304
  const sdkHeaders = { ...headers }
305
305
  if (account.basicAuth) {
306
306
  sdkHeaders.Authorization = account.basicAuth
307
+ sdkHeaders.Authentication = account.basicAuth
307
308
  }
308
309
 
309
310
  sdk.setConfig({
package/src/self-check.js CHANGED
@@ -47,16 +47,22 @@ try {
47
47
  'data-lang="en"',
48
48
  'data-group-tab="login"',
49
49
  'data-group-tab="register"',
50
+ 'data-method="mobile-password"',
50
51
  'data-method="basic"',
51
52
  'data-method="x-token"',
52
53
  'data-method="register-mobile"',
53
54
  'merchantNo',
54
- 'merchantKey'
55
+ 'merchantKey',
56
+ 'loginAdminMobile',
57
+ 'Authentication'
55
58
  ]) {
56
59
  if (!authPage.includes(expected)) {
57
60
  throw new Error(`Authorization page is missing ${expected}.`)
58
61
  }
59
62
  }
63
+ if (authPage.includes('data-method="username-password"')) {
64
+ throw new Error('Authorization page must not expose username-password login.')
65
+ }
60
66
 
61
67
  const mobileAuth = await client.callTool({
62
68
  name: 'apifm_admin_start_auth',