@reconcrap/boss-recommend-mcp 2.1.10 → 2.1.12

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
@@ -100,6 +100,7 @@ MCP 工具:
100
100
  - `resume_recruit_pipeline_run`
101
101
  - `cancel_recruit_pipeline_run`
102
102
  - `boss_chat_health_check`
103
+ - `list_boss_chat_jobs`(只读读取聊天页岗位列表;chat-only 获取 `job_options` 的首选别名,不会启动任务)
103
104
  - `prepare_boss_chat_run`
104
105
  - `start_boss_chat_run`
105
106
  - `get_boss_chat_run`
@@ -224,8 +225,9 @@ node src/cli.js start
224
225
  如果检测到 legacy Boss server entries,installer 会:
225
226
 
226
227
  - 保留非 Boss MCP server。
227
- - 默认写入统一 server:`boss-recommend -> npx -y @reconcrap/boss-recommend-mcp@<installed-version> start`
228
- - 如果传入 `--mcp-launch global-wrapper`,写入升级稳定入口:`boss-recommend -> ~/.boss-recommend-mcp/bin/boss-recommend-mcp-mcp-server`。该 wrapper 会加载 `~/.nvm/nvm.sh` 并执行当前全局 `boss-recommend-mcp start`,适合 macOS 上通过 `npm -g i @reconcrap/boss-recommend-mcp@latest` 持续升级。
228
+ - Trae/Trae-CN 默认写入三个小 toolset server:`boss-recommend`(`BOSS_RECOMMEND_MCP_TOOLSET=recommend`)、`boss-chat`(`chat`)、`boss-recruit`(`recruit`)。这样 recommend/chat/search 的 tool list 不会互相挤占 agent 可见工具预算。
229
+ - 其它宿主默认仍写入兼容统一 server:`boss-recommend -> npx -y @reconcrap/boss-recommend-mcp@<installed-version> start`。
230
+ - 如果传入 `--mcp-launch global-wrapper`,Trae/Trae-CN 同样会写入三个 toolset server,但 command 指向升级稳定 wrapper。该 wrapper 会加载 `~/.nvm/nvm.sh` 并执行当前全局 `boss-recommend-mcp start`,适合 macOS 上通过 `npm -g i @reconcrap/boss-recommend-mcp@latest` 持续升级。
229
231
  - 从同一个 `mcp.json` 删除旧 `boss-recruit-mcp`、standalone `boss-chat`、旧本地 Boss repo 路径,避免 agent 继续调用 legacy 包。
230
232
  - 在原文件旁生成 `mcp.json.boss-mcp-migration-*.bak`。
231
233
  - 同步外部 skills 目录里的 `boss-recommend-pipeline`、`boss-recruit-pipeline`、`boss-chat`。
@@ -262,6 +264,7 @@ BOSS_RECOMMEND_EXTERNAL_SKILL_DIRS="/path/to/skills" boss-recommend-mcp install
262
264
  BOSS_RECOMMEND_HOME # 统一状态目录,默认 ~/.boss-recommend-mcp
263
265
  BOSS_CHAT_HOME # 覆盖 boss-chat 运行态目录;默认 ~/.boss-recommend-mcp/boss-chat
264
266
  BOSS_RECOMMEND_SCREEN_CONFIG # 显式指定 screening-config.json 路径(最高优先级)
267
+ BOSS_RECOMMEND_MCP_TOOLSET # 可选收窄 MCP 工具:all|recommend|chat|recruit;Trae/Trae-CN installer 会自动设置
265
268
  BOSS_RECOMMEND_MCP_CONFIG_TARGETS # JSON 数组或系统 path 分隔路径列表,指定额外 mcp.json 目标文件
266
269
  BOSS_RECOMMEND_EXTERNAL_SKILL_DIRS # JSON 数组或系统 path 分隔路径列表,指定额外 skills 根目录
267
270
  ```
@@ -424,7 +427,7 @@ node src/cli.js chat prepare-run --slow-live --port 9222
424
427
  - `baseUrl` / `apiKey` / `model` 不再单独传入,固定复用 recommend 的 `screening-config.json`
425
428
  - `greeting_text` 默认优先级:本次显式值 > `screening-config.json.greetingMessage` > 内置默认招呼语(`Hi同学,能麻烦发下简历吗?`)
426
429
  - 若缺少 `follow_up.chat` 必填项,pipeline 会返回 `NEED_INPUT`
427
- - 如需聊天页筛选,请调用 `prepare_boss_chat_run` 获取岗位列表,再调用 `start_boss_chat_run`。
430
+ - 如需聊天页筛选,请调用 `list_boss_chat_jobs` 或 `prepare_boss_chat_run` 获取岗位列表,再调用 `start_boss_chat_run`。chat-only、未读、全部聊天、求简历任务不要调用 `list_recommend_jobs` / `run_recommend` / `start_recommend_pipeline_run`;这些 recommend 工具会对明确的 chat/search 误路由 fail closed。
428
431
  - `boss-chat` 状态统一写入 `~/.boss-recommend-mcp/boss-chat`(或 `BOSS_CHAT_HOME` 指定目录),不再依赖工作区 `cwd`
429
432
 
430
433
  ## Chat-only
@@ -438,6 +441,7 @@ node src/cli.js chat prepare-run --slow-live --port 9222
438
441
  - `boss-recommend-mcp chat get-run|pause-run|resume-run|cancel-run`
439
442
  - MCP:
440
443
  - `boss_chat_health_check`
444
+ - `list_boss_chat_jobs`
441
445
  - `prepare_boss_chat_run`
442
446
  - `start_boss_chat_run`
443
447
  - `get_boss_chat_run`
@@ -450,14 +454,15 @@ node src/cli.js chat prepare-run --slow-live --port 9222
450
454
 
451
455
  chat-only 交互建议:
452
456
 
453
- - 先调用一次 `prepare_boss_chat_run`(可不带参数),服务会先导航到 `https://www.zhipin.com/web/chat/index` 并返回 `NEED_INPUT`,其中包含岗位 `job_options` 与待补字段。
457
+ - 先调用一次 `list_boss_chat_jobs` 或 `prepare_boss_chat_run`(可不带参数),服务会先导航到 `https://www.zhipin.com/web/chat/index` 并返回 `NEED_INPUT`,其中包含岗位 `job_options` 与待补字段。
454
458
  - 然后基于 `job_options` 让用户选择 `job`,并补齐 `start_from`、`target_count`、`criteria` 后调用 `start_boss_chat_run` 启动任务。
455
459
  - `greeting_text` 可选;未传时使用 `screening-config.json.greetingMessage`,若未配置则使用默认招呼语(`Hi同学,能麻烦发下简历吗?`)。
456
460
  - `target_count` 支持正整数、`all`、`-1`;若用户给出 `全部候选人` / `所有候选人`,会自动按不限(扫到底)处理。
457
461
 
458
462
  Trae-CN / 长对话防循环建议:
459
463
 
460
- - 固定流程:`boss_chat_health_check` -> `prepare_boss_chat_run(空参可)` -> 一次性补齐 `job/start_from/target_count/criteria` -> `start_boss_chat_run`。
464
+ - 固定流程:`boss_chat_health_check` -> `list_boss_chat_jobs(空参可)` / `prepare_boss_chat_run(空参可)` -> 一次性补齐 `job/start_from/target_count/criteria` -> `start_boss_chat_run`。
465
+ - chat-only 场景严禁调用 `list_recommend_jobs`、`run_recommend` 或 `start_recommend_pipeline_run`。
461
466
  - `start_boss_chat_run` 的工具 schema 已把 `job/start_from/target_count/criteria` 标记为必填;不要用它获取岗位列表。
462
467
  - 若 `pending_questions` / UI 选项里出现“扫到底(必须传 `target_count="all"`)”,下一次工具调用请直接照抄 `"target_count": "all"`,不要只保留“扫到底”这层自然语言语义。
463
468
  - `start_boss_chat_run` 返回 `ACCEPTED` 后直接结束当前回合,不要自动轮询。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "2.1.10",
3
+ "version": "2.1.12",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -16,7 +16,7 @@ Please run a Boss chat-only task (do not switch to recommend flow).
16
16
 
17
17
  Execution order:
18
18
  1) Call boss_chat_health_check.
19
- 2) Call prepare_boss_chat_run once (empty params allowed) to fetch job_options and missing fields.
19
+ 2) Call list_boss_chat_jobs once (empty params allowed), or prepare_boss_chat_run once, to fetch job_options and missing fields.
20
20
  3) Ask for these required fields in one shot: job, start_from (unread/all), target_count, criteria, rest_level (low/medium/high).
21
21
  4) After user reply, call start_boss_chat_run exactly once to start the run.
22
22
  5) If ACCEPTED, reply only with run_id and "task started"; no auto polling.
@@ -24,6 +24,8 @@ Execution order:
24
24
  Anti-loop rules:
25
25
  - Do not repeat the same sentence across turns.
26
26
  - On validation errors, list all missing/invalid fields once.
27
+ - Do not call list_recommend_jobs for chat-only tasks; it is recommend-page only and will switch the browser to /web/chat/recommend.
28
+ - Do not call run_recommend or start_recommend_pipeline_run for chat-only tasks; use start_boss_chat_run.
27
29
  - Do not use start_boss_chat_run for preflight. It is only for the final start call and must include job/start_from/target_count/criteria.
28
30
  - Do not call start_boss_chat_run repeatedly in one turn.
29
31
  - Do not call get_boss_chat_run unless user explicitly asks for progress.
@@ -13,7 +13,10 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
13
13
 
14
14
  ## Tool Routing
15
15
 
16
+ Trae/Trae-CN split-server config exposes these under the `boss-chat` MCP server. Chat-only tasks should call `boss-chat/<tool>` when the host shows server-qualified tool names.
17
+
16
18
  - 健康检查:`boss_chat_health_check`
19
+ - 只读获取聊天页岗位列表:`list_boss_chat_jobs`
17
20
  - 预备并获取岗位列表:`prepare_boss_chat_run`
18
21
  - 启动异步任务:`start_boss_chat_run`
19
22
  - 查询进度:`get_boss_chat_run`
@@ -69,6 +72,8 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
69
72
  - 若本机找不到 Chrome,可提示用户设置 `BOSS_MCP_CHROME_PATH` 或 `BOSS_RECOMMEND_CHROME_PATH`;非本机 debug host 不自动启动。
70
73
  - `job` / `start_from` / `criteria` 缺一不可;缺参时只补缺口。
71
74
  - `target_count` 在 chat-only 启动前也是必填项,不能默认省略。
75
+ - chat-only 岗位列表只能通过 `list_boss_chat_jobs` 或 `prepare_boss_chat_run` 获取;严禁调用 `list_recommend_jobs`,因为它会切到推荐页。
76
+ - chat-only 启动只能调用 `start_boss_chat_run`;严禁调用 `run_recommend` 或 `start_recommend_pipeline_run`。
72
77
  - 每次 run 必须明确询问用户本次休息强度 `rest_level`:`low`(旧策略)/ `medium`(约 5 小时或 700 人累计休息 30 分钟)/ `high`(约 5 小时或 700 人累计休息 1 小时);不得默认使用配置文件里的值替用户决定。
73
78
  - 当用户说“全部候选人/所有候选人”时,必须按“扫到底(unlimited)”处理,不要再追问正整数。
74
79
  - 参数名必须写 `target_count`(不要写“目标数量”等中文键名)。
@@ -77,7 +82,7 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
77
82
  - 若工具或提问选项里出现“扫到底(必须传 `target_count=\"all\"`)”之类字样,下一次工具调用时必须直接照抄这个字面量,不要只保留“扫到底”语义。
78
83
  - 禁止 agent 自行补全 `job/start_from/criteria` 并直接执行,必须由用户明确给出或确认。
79
84
  - chat-only 启动流程必须先进入聊天页并拉取岗位列表,再让用户从列表中选择 `job`。
80
- - 必须先用空参调用 `prepare_boss_chat_run` 获取 `job_options`;不要用 `start_boss_chat_run` 做预备调用。
85
+ - 必须先用空参调用 `list_boss_chat_jobs` 或 `prepare_boss_chat_run` 获取 `job_options`;不要用 `start_boss_chat_run` 做预备调用。
81
86
  - `start_boss_chat_run` 只能用于真正启动,必须一次性传齐 `job` / `start_from` / `target_count` / `criteria`。
82
87
  - 若 `start_boss_chat_run` 返回 `NEED_INPUT` 且 `missing_fields` 包含 `target_count`,说明你没有把用户选择写入工具参数;下一次调用必须照 `next_call_example` 原样补上 `"target_count": "all"` 或正整数,不要重复空调用。
83
88
  - 默认不自动轮询;只有用户要求查进度时才调用 `get_boss_chat_run`。
@@ -96,6 +101,6 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
96
101
  ## Response Style
97
102
 
98
103
  - 用结构化中文。
99
- - 首轮建议先调用一次 `prepare_boss_chat_run`(可空参)获取 `job_options` 与 `pending_questions`。
104
+ - 首轮建议先调用一次 `list_boss_chat_jobs`(可空参)或 `prepare_boss_chat_run`(可空参)获取 `job_options` 与 `pending_questions`。
100
105
  - 缺参时必须逐项确认:`job`(来自岗位列表)、`start_from`(`unread|all`)、`target_count`、`criteria`、`rest_level`。
101
106
  - 若健康检查失败,明确提示共享配置文件 `screening-config.json` 不可用。
@@ -97,6 +97,8 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
97
97
 
98
98
  ## Tool Usage
99
99
 
100
+ Trae/Trae-CN split-server config exposes these under the `boss-recommend` MCP server. 推荐页任务应调用 `boss-recommend/<tool>`;不要切到 `boss-chat` 或 `boss-recruit`。
101
+
100
102
  - 岗位发现工具:`list_recommend_jobs`
101
103
  - 用途:当用户需要为 cron / 一次性自动任务提前填写完整参数时,先用它读取推荐页岗位下拉框的全部可用岗位名;默认会复用/自动打开本机 9222 Chrome 并导航到推荐页。
102
104
  - 输出:优先把 `job_names` 里的值作为后续 `overrides.job` / `confirmation.job_value`。
@@ -11,6 +11,8 @@ description: "Use when users want Boss search/recruit-page screening via the uni
11
11
 
12
12
  ## Tool Routing
13
13
 
14
+ Trae/Trae-CN split-server config exposes these under the `boss-recruit` MCP server. Search/recruit tasks should call `boss-recruit/<tool>` when the host shows server-qualified tool names.
15
+
14
16
  - 同步启动:`run_recruit_pipeline`
15
17
  - 异步启动:`start_recruit_pipeline_run`
16
18
  - 查询进度:`get_recruit_pipeline_run`
package/src/cli.js CHANGED
@@ -57,12 +57,15 @@ const bossLoginUrl = "https://www.zhipin.com/web/user/?ka=bticket";
57
57
  const chromeOnboardingUrlPattern = /^chrome:\/\/(welcome|intro|newtab|signin|history-sync|settings\/syncSetup)/i;
58
58
  const bossLoginUrlPattern = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com)/i;
59
59
  const bossLoginTitlePattern = /登录|signin|扫码登录|BOSS直聘登录/i;
60
- const supportedMcpClients = ["generic", "cursor", "trae", "claudecode", "openclaw", "qclaw"];
60
+ const supportedMcpClients = ["generic", "cursor", "trae", "claudecode", "openclaw", "qclaw"];
61
61
  const defaultMcpServerName = "boss-recommend";
62
+ const bossChatMcpServerName = "boss-chat";
63
+ const bossRecruitMcpServerName = "boss-recruit";
62
64
  const defaultMcpCommand = "npx";
63
65
  const recommendMcpPackageName = "@reconcrap/boss-recommend-mcp";
64
66
  const recommendMcpBinaryName = "boss-recommend-mcp";
65
67
  const globalMcpWrapperFileName = "boss-recommend-mcp-mcp-server";
68
+ const mcpToolsetEnv = "BOSS_RECOMMEND_MCP_TOOLSET";
66
69
  const supportedMcpLaunchModes = ["npx", "global-wrapper"];
67
70
  const autoSyncSkipCommands = new Set(["install", "install-skill", "where", "help", "--help", "-h", "list-jobs", "jobs", "recommend-jobs"]);
68
71
  const externalMcpTargetsEnv = "BOSS_RECOMMEND_MCP_CONFIG_TARGETS";
@@ -807,12 +810,35 @@ function shouldDefaultRecommendDetachedMcpEnv(options = {}) {
807
810
  return client === "openclaw"
808
811
  || client === "qclaw"
809
812
  || agent === "openclaw"
810
- || agent === "qclaw";
811
- }
812
-
813
- function getDefaultMcpEnv(options = {}) {
814
- return shouldDefaultRecommendDetachedMcpEnv(options)
815
- ? { ...detachedRecommendMcpEnv }
813
+ || agent === "qclaw";
814
+ }
815
+
816
+ function normalizeMcpToolsetOption(value) {
817
+ const raw = String(value || "").trim().toLowerCase().replace(/[\s_]+/g, "-");
818
+ if (!raw || raw === "all") return "";
819
+ if (raw === "boss-recommend" || raw === "recommend-page") return "recommend";
820
+ if (raw === "boss-chat" || raw === "chat-only" || raw === "chat-page") return "chat";
821
+ if (raw === "boss-recruit" || raw === "search" || raw === "search-page" || raw === "recruit-page") return "recruit";
822
+ if (["recommend", "chat", "recruit"].includes(raw)) return raw;
823
+ throw new Error(`Unsupported --toolset value: ${raw}. Supported: recommend, chat, recruit, all`);
824
+ }
825
+
826
+ function getMcpToolsetEnv(options = {}) {
827
+ const toolset = normalizeMcpToolsetOption(options.toolset || options["toolset"]);
828
+ return toolset ? { [mcpToolsetEnv]: toolset } : {};
829
+ }
830
+
831
+ function shouldUseSplitBossMcpServers(options = {}) {
832
+ if (options["server-name"] || options.serverName || options.toolset || options["toolset"]) return false;
833
+ if (options.split === true || options["split-tools"] === true || options.splitTools === true) return true;
834
+ const client = normalizeMcpClientName(options.client);
835
+ const agent = normalizeAgentName(options.agent);
836
+ return client === "trae" || agent === "trae" || agent === "trae-cn";
837
+ }
838
+
839
+ function getDefaultMcpEnv(options = {}) {
840
+ return shouldDefaultRecommendDetachedMcpEnv(options)
841
+ ? { ...detachedRecommendMcpEnv }
816
842
  : {};
817
843
  }
818
844
 
@@ -834,6 +860,7 @@ function buildMcpLaunchConfig(options = {}) {
834
860
  };
835
861
  const mergedEnv = {
836
862
  ...getDefaultMcpEnv(options),
863
+ ...getMcpToolsetEnv(options),
837
864
  ...(isPlainObject(env) ? env : {})
838
865
  };
839
866
  if (Object.keys(mergedEnv).length > 0) {
@@ -853,19 +880,36 @@ function buildMcpLaunchConfig(options = {}) {
853
880
  ? ["start"]
854
881
  : buildDefaultMcpArgs(options);
855
882
  const launchConfig = { command, args: launchArgs };
856
- const mergedEnv = {
857
- ...getDefaultMcpEnv(options),
858
- ...(isPlainObject(env) ? env : {})
859
- };
883
+ const mergedEnv = {
884
+ ...getDefaultMcpEnv(options),
885
+ ...getMcpToolsetEnv(options),
886
+ ...(isPlainObject(env) ? env : {})
887
+ };
860
888
  if (Object.keys(mergedEnv).length > 0) {
861
889
  launchConfig.env = mergedEnv;
862
890
  }
863
891
  return launchConfig;
864
- }
865
-
866
- function mergeExistingMcpEntryEnv(existingEntry, launchConfig) {
867
- if (!isPlainObject(existingEntry?.env) || !isPlainObject(launchConfig)) {
868
- return launchConfig;
892
+ }
893
+
894
+ function buildBossMcpServerEntries(options = {}) {
895
+ if (shouldUseSplitBossMcpServers(options)) {
896
+ return {
897
+ [defaultMcpServerName]: buildMcpLaunchConfig({ ...options, toolset: "recommend" }),
898
+ [bossChatMcpServerName]: buildMcpLaunchConfig({ ...options, toolset: "chat" }),
899
+ [bossRecruitMcpServerName]: buildMcpLaunchConfig({ ...options, toolset: "recruit" })
900
+ };
901
+ }
902
+ const serverName = typeof options["server-name"] === "string" && options["server-name"].trim()
903
+ ? options["server-name"].trim()
904
+ : defaultMcpServerName;
905
+ return {
906
+ [serverName]: buildMcpLaunchConfig(options)
907
+ };
908
+ }
909
+
910
+ function mergeExistingMcpEntryEnv(existingEntry, launchConfig) {
911
+ if (!isPlainObject(existingEntry?.env) || !isPlainObject(launchConfig)) {
912
+ return launchConfig;
869
913
  }
870
914
  return {
871
915
  ...launchConfig,
@@ -874,28 +918,21 @@ function mergeExistingMcpEntryEnv(existingEntry, launchConfig) {
874
918
  ...(isPlainObject(launchConfig.env) ? launchConfig.env : {})
875
919
  }
876
920
  };
877
- }
878
-
879
- function buildMcpConfigFileContent(options = {}) {
880
- const serverName = typeof options["server-name"] === "string" && options["server-name"].trim()
881
- ? options["server-name"].trim()
882
- : defaultMcpServerName;
883
- const launchConfig = buildMcpLaunchConfig(options);
884
- if (normalizeMcpClientName(options.client) === "qclaw") {
885
- return {
886
- mcp: {
887
- servers: {
888
- [serverName]: launchConfig
889
- }
890
- }
891
- };
892
- }
893
- return {
894
- mcpServers: {
895
- [serverName]: launchConfig
896
- }
897
- };
898
- }
921
+ }
922
+
923
+ function buildMcpConfigFileContent(options = {}) {
924
+ const servers = buildBossMcpServerEntries(options);
925
+ if (normalizeMcpClientName(options.client) === "qclaw") {
926
+ return {
927
+ mcp: {
928
+ servers
929
+ }
930
+ };
931
+ }
932
+ return {
933
+ mcpServers: servers
934
+ };
935
+ }
899
936
 
900
937
  function writeMcpConfigFiles(options = {}) {
901
938
  const clients = parseMcpClientTargets(options.client);
@@ -1039,25 +1076,28 @@ function getMcpServersFromConfig(config = {}, useQClawShape = false) {
1039
1076
  return {};
1040
1077
  }
1041
1078
 
1042
- function mergeMcpServerConfigFile(filePath, options = {}) {
1043
- const current = readJsonObjectFileSafe(filePath);
1044
- const useQClawShape = isQClawMcpConfigTarget(filePath, options, current);
1045
- const nextConfig = buildMcpConfigFileContent({ ...options, client: useQClawShape ? "qclaw" : options.client });
1046
- const nextServers = useQClawShape ? nextConfig.mcp?.servers : nextConfig.mcpServers;
1047
- const serverName = Object.keys(nextServers || {})[0] || defaultMcpServerName;
1048
- const existingServers = getMcpServersFromConfig(current, useQClawShape);
1049
- const existingEntry = existingServers[serverName];
1050
- const launchConfig = mergeExistingMcpEntryEnv(
1051
- existingEntry,
1052
- nextServers?.[serverName] || buildMcpLaunchConfig(options)
1053
- );
1054
- const retainedServers = {};
1055
- const migratedLegacyServers = [];
1056
- for (const [name, config] of Object.entries(existingServers)) {
1057
- if (name === serverName) continue;
1058
- if (isBossMcpServerEntry(name, config)) {
1059
- migratedLegacyServers.push(name);
1060
- continue;
1079
+ function mergeMcpServerConfigFile(filePath, options = {}) {
1080
+ const current = readJsonObjectFileSafe(filePath);
1081
+ const useQClawShape = isQClawMcpConfigTarget(filePath, options, current);
1082
+ const nextConfig = buildMcpConfigFileContent({ ...options, client: useQClawShape ? "qclaw" : options.client });
1083
+ const nextServers = useQClawShape ? nextConfig.mcp?.servers : nextConfig.mcpServers;
1084
+ const serverNames = Object.keys(nextServers || {});
1085
+ const nextBossServerNames = new Set(serverNames);
1086
+ const existingServers = getMcpServersFromConfig(current, useQClawShape);
1087
+ const mergedBossServers = {};
1088
+ for (const serverName of serverNames) {
1089
+ mergedBossServers[serverName] = mergeExistingMcpEntryEnv(
1090
+ existingServers[serverName],
1091
+ nextServers?.[serverName] || buildMcpLaunchConfig(options)
1092
+ );
1093
+ }
1094
+ const retainedServers = {};
1095
+ const migratedLegacyServers = [];
1096
+ for (const [name, config] of Object.entries(existingServers)) {
1097
+ if (nextBossServerNames.has(name)) continue;
1098
+ if (isBossMcpServerEntry(name, config)) {
1099
+ migratedLegacyServers.push(name);
1100
+ continue;
1061
1101
  }
1062
1102
  retainedServers[name] = config;
1063
1103
  }
@@ -1065,20 +1105,20 @@ function mergeMcpServerConfigFile(filePath, options = {}) {
1065
1105
  ? {
1066
1106
  ...current,
1067
1107
  mcp: {
1068
- ...(current?.mcp && typeof current.mcp === "object" && !Array.isArray(current.mcp) ? current.mcp : {}),
1069
- servers: {
1070
- ...retainedServers,
1071
- [serverName]: launchConfig
1072
- }
1073
- }
1074
- }
1075
- : {
1076
- ...current,
1077
- mcpServers: {
1078
- ...retainedServers,
1079
- [serverName]: launchConfig
1080
- }
1081
- };
1108
+ ...(current?.mcp && typeof current.mcp === "object" && !Array.isArray(current.mcp) ? current.mcp : {}),
1109
+ servers: {
1110
+ ...retainedServers,
1111
+ ...mergedBossServers
1112
+ }
1113
+ }
1114
+ }
1115
+ : {
1116
+ ...current,
1117
+ mcpServers: {
1118
+ ...retainedServers,
1119
+ ...mergedBossServers
1120
+ }
1121
+ };
1082
1122
 
1083
1123
  ensureDir(path.dirname(filePath));
1084
1124
  const before = pathExists(filePath) ? fs.readFileSync(filePath, "utf8") : "";
@@ -1088,14 +1128,15 @@ function mergeMcpServerConfigFile(filePath, options = {}) {
1088
1128
  backupFile = `${filePath}.boss-mcp-migration-${new Date().toISOString().replace(/[:.]/g, "-")}.bak`;
1089
1129
  fs.writeFileSync(backupFile, before, "utf8");
1090
1130
  }
1091
- fs.writeFileSync(filePath, JSON.stringify(merged, null, 2), "utf8");
1092
- const updated = before.trim() !== next.trim() || JSON.stringify(existingEntry || null) !== JSON.stringify(launchConfig);
1093
- return {
1094
- file: filePath,
1095
- server: serverName,
1096
- config_shape: useQClawShape ? "qclaw" : "mcpServers",
1097
- updated,
1098
- migrated_legacy_servers: migratedLegacyServers,
1131
+ fs.writeFileSync(filePath, JSON.stringify(merged, null, 2), "utf8");
1132
+ const updated = before.trim() !== next.trim();
1133
+ return {
1134
+ file: filePath,
1135
+ server: serverNames[0] || defaultMcpServerName,
1136
+ servers: serverNames,
1137
+ config_shape: useQClawShape ? "qclaw" : "mcpServers",
1138
+ updated,
1139
+ migrated_legacy_servers: migratedLegacyServers,
1099
1140
  backup_file: backupFile
1100
1141
  };
1101
1142
  }
@@ -1108,12 +1149,13 @@ function installExternalMcpConfigs(options = {}) {
1108
1149
  try {
1109
1150
  const existed = pathExists(target);
1110
1151
  const merged = mergeMcpServerConfigFile(target, options);
1111
- applied.push({
1112
- file: target,
1113
- server: merged.server,
1114
- created: !existed,
1115
- updated: merged.updated,
1116
- migrated_legacy_servers: merged.migrated_legacy_servers,
1152
+ applied.push({
1153
+ file: target,
1154
+ server: merged.server,
1155
+ servers: merged.servers,
1156
+ created: !existed,
1157
+ updated: merged.updated,
1158
+ migrated_legacy_servers: merged.migrated_legacy_servers,
1117
1159
  backup_file: merged.backup_file
1118
1160
  });
1119
1161
  } catch (error) {
@@ -2718,6 +2760,7 @@ function printHelp() {
2718
2760
  console.log(" boss-recommend-mcp launch-chrome Launch or reuse Chrome debug instance and open Boss recommend page");
2719
2761
  console.log(" boss-recommend-mcp where Print installed package, skill, and config paths");
2720
2762
  console.log(" boss-recommend-mcp install --mcp-launch global-wrapper Use ~/.boss-recommend-mcp/bin wrapper so npm global upgrades affect MCP hosts");
2763
+ console.log(" boss-recommend-mcp start --toolset recommend Start a narrowed MCP server (all|recommend|chat|recruit)");
2721
2764
  console.log("");
2722
2765
  console.log("Run command:");
2723
2766
  console.log(" boss-recommend-mcp prepare-run --instruction \"...\" --overrides-file overrides.json --confirmation-file confirmation.json --rest-level medium");
@@ -3397,6 +3440,7 @@ export const __testables = {
3397
3440
  buildBossChatCliInput,
3398
3441
  buildDefaultMcpArgs,
3399
3442
  buildMcpLaunchConfig,
3443
+ buildMcpConfigFileContent,
3400
3444
  ensureGlobalMcpWrapper,
3401
3445
  getGlobalMcpWrapperPath,
3402
3446
  collectRuntimeDirectories,
package/src/index.js CHANGED
@@ -110,6 +110,7 @@ const TOOL_RUN_FEATURED_CALIBRATION = "run_featured_calibration";
110
110
  const TOOL_GET_FEATURED_CALIBRATION_STATUS = "get_featured_calibration_status";
111
111
  const TOOL_RUN_RECOMMEND_SELF_HEAL = "run_recommend_self_heal";
112
112
  const TOOL_BOSS_CHAT_HEALTH_CHECK = "boss_chat_health_check";
113
+ const TOOL_BOSS_CHAT_LIST_JOBS = "list_boss_chat_jobs";
113
114
  const TOOL_BOSS_CHAT_PREPARE_RUN = "prepare_boss_chat_run";
114
115
  const TOOL_BOSS_CHAT_START_RUN = "start_boss_chat_run";
115
116
  const TOOL_BOSS_CHAT_GET_RUN = "get_boss_chat_run";
@@ -122,8 +123,50 @@ const TOOL_GET_RECRUIT_PIPELINE_RUN = "get_recruit_pipeline_run";
122
123
  const TOOL_CANCEL_RECRUIT_PIPELINE_RUN = "cancel_recruit_pipeline_run";
123
124
  const TOOL_PAUSE_RECRUIT_PIPELINE_RUN = "pause_recruit_pipeline_run";
124
125
  const TOOL_RESUME_RECRUIT_PIPELINE_RUN = "resume_recruit_pipeline_run";
125
-
126
- const SERVER_NAME = "boss-recommend-mcp";
126
+
127
+ const SERVER_NAME = "boss-recommend-mcp";
128
+ const MCP_TOOLSET_ENV = "BOSS_RECOMMEND_MCP_TOOLSET";
129
+ const MCP_TOOLSET_ALL = "all";
130
+ const MCP_TOOLSET_RECOMMEND = "recommend";
131
+ const MCP_TOOLSET_CHAT = "chat";
132
+ const MCP_TOOLSET_RECRUIT = "recruit";
133
+ const VALID_MCP_TOOLSETS = new Set([
134
+ MCP_TOOLSET_ALL,
135
+ MCP_TOOLSET_RECOMMEND,
136
+ MCP_TOOLSET_CHAT,
137
+ MCP_TOOLSET_RECRUIT
138
+ ]);
139
+ const RECOMMEND_TOOL_NAMES = new Set([
140
+ TOOL_LIST_RECOMMEND_JOBS,
141
+ TOOL_RUN_RECOMMEND,
142
+ TOOL_START_RUN,
143
+ TOOL_PREPARE_RUN,
144
+ TOOL_SCHEDULE_RUN,
145
+ TOOL_GET_SCHEDULED_RUN,
146
+ TOOL_GET_RUN,
147
+ TOOL_LIST_RUNS,
148
+ TOOL_CANCEL_RUN,
149
+ TOOL_PAUSE_RUN,
150
+ TOOL_RESUME_RUN
151
+ ]);
152
+ const BOSS_CHAT_TOOL_NAMES = new Set([
153
+ TOOL_BOSS_CHAT_HEALTH_CHECK,
154
+ TOOL_BOSS_CHAT_LIST_JOBS,
155
+ TOOL_BOSS_CHAT_PREPARE_RUN,
156
+ TOOL_BOSS_CHAT_START_RUN,
157
+ TOOL_BOSS_CHAT_GET_RUN,
158
+ TOOL_BOSS_CHAT_PAUSE_RUN,
159
+ TOOL_BOSS_CHAT_RESUME_RUN,
160
+ TOOL_BOSS_CHAT_CANCEL_RUN
161
+ ]);
162
+ const RECRUIT_TOOL_NAMES = new Set([
163
+ TOOL_RUN_RECRUIT_PIPELINE,
164
+ TOOL_START_RECRUIT_PIPELINE_RUN,
165
+ TOOL_GET_RECRUIT_PIPELINE_RUN,
166
+ TOOL_CANCEL_RECRUIT_PIPELINE_RUN,
167
+ TOOL_PAUSE_RECRUIT_PIPELINE_RUN,
168
+ TOOL_RESUME_RECRUIT_PIPELINE_RUN
169
+ ]);
127
170
  const FRAMING_UNKNOWN = "unknown";
128
171
  const FRAMING_HEADER = "header";
129
172
  const FRAMING_LINE = "line";
@@ -453,6 +496,26 @@ function readRawRunState(runId) {
453
496
  }
454
497
  }
455
498
 
499
+ function compactProgressForList(progress = {}) {
500
+ const compact = {};
501
+ for (const key of [
502
+ "processed",
503
+ "screened",
504
+ "passed",
505
+ "skipped",
506
+ "target_count",
507
+ "card_count",
508
+ "detail_opened",
509
+ "greet_count",
510
+ "post_action_clicked"
511
+ ]) {
512
+ if (Number.isFinite(progress?.[key])) {
513
+ compact[key] = progress[key];
514
+ }
515
+ }
516
+ return compact;
517
+ }
518
+
456
519
  function getRunSortTime(run = {}, fallbackMs = 0) {
457
520
  for (const key of ["updated_at", "heartbeat_at", "completed_at", "started_at", "updatedAt", "completedAt", "startedAt"]) {
458
521
  const ms = Date.parse(run?.[key] || "");
@@ -476,7 +539,7 @@ function compactRunForList(run = {}) {
476
539
  heartbeat_at: run.heartbeat_at || null,
477
540
  completed_at: run.completed_at || run.completedAt || null,
478
541
  pid: Number.isInteger(run.pid) && run.pid > 0 ? run.pid : null,
479
- progress: run.progress || {},
542
+ progress: compactProgressForList(run.progress),
480
543
  last_message: normalizeText(run.last_message || error?.message || ""),
481
544
  control: {
482
545
  pause_requested: run.control?.pause_requested === true,
@@ -493,11 +556,10 @@ function compactRunForList(run = {}) {
493
556
  report_json: normalizeText(result.report_json || result.result?.report_json || ""),
494
557
  checkpoint_path: normalizeText(result.checkpoint_path || result.result?.checkpoint_path || "")
495
558
  } : null,
496
- resume: {
497
- checkpoint_path: normalizeText(run.resume?.checkpoint_path || ""),
498
- output_csv: normalizeText(run.resume?.output_csv || ""),
499
- worker_stdout_path: normalizeText(run.resume?.worker_stdout_path || ""),
500
- worker_stderr_path: normalizeText(run.resume?.worker_stderr_path || "")
559
+ artifacts: {
560
+ output_csv: normalizeText(result?.output_csv || result?.result?.output_csv || ""),
561
+ report_json: normalizeText(result?.report_json || result?.result?.report_json || ""),
562
+ checkpoint_path: normalizeText(result?.checkpoint_path || result?.result?.checkpoint_path || "")
501
563
  }
502
564
  };
503
565
  }
@@ -1293,32 +1355,276 @@ function createScheduleRunInputSchema() {
1293
1355
  };
1294
1356
  }
1295
1357
 
1296
- function createToolsSchema() {
1297
- return [
1358
+ function createCompactRunInputSchema() {
1359
+ const targetCountSchema = {
1360
+ anyOf: [
1361
+ { type: "integer", minimum: 0 },
1362
+ { type: "string" }
1363
+ ],
1364
+ description: "目标通过人数;扫到底/不限可传 all"
1365
+ };
1366
+ return {
1367
+ type: "object",
1368
+ properties: {
1369
+ instruction: {
1370
+ type: "string",
1371
+ description: "用户原始筛选标准/任务说明;正式启动时逐字复用"
1372
+ },
1373
+ confirmation: {
1374
+ type: "object",
1375
+ properties: {
1376
+ final_confirmed: {
1377
+ type: "boolean",
1378
+ description: "用户完成总确认后传 true"
1379
+ }
1380
+ },
1381
+ additionalProperties: true
1382
+ },
1383
+ overrides: {
1384
+ type: "object",
1385
+ properties: {
1386
+ page_scope: { type: "string", enum: ["recommend", "latest", "featured"] },
1387
+ school_tag: {
1388
+ anyOf: [
1389
+ { type: "string" },
1390
+ { type: "array", items: { type: "string" } }
1391
+ ]
1392
+ },
1393
+ degree: {
1394
+ anyOf: [
1395
+ { type: "string" },
1396
+ { type: "array", items: { type: "string" } }
1397
+ ]
1398
+ },
1399
+ gender: { type: "string" },
1400
+ recent_not_view: { type: "string" },
1401
+ criteria: { type: "string" },
1402
+ target_count: targetCountSchema,
1403
+ post_action: { type: "string", enum: ["greet", "none"] },
1404
+ max_greet_count: targetCountSchema,
1405
+ job: { type: "string" }
1406
+ },
1407
+ additionalProperties: true
1408
+ },
1409
+ human_behavior: {
1410
+ type: "object",
1411
+ properties: {
1412
+ restLevel: { type: "string", enum: ["low", "medium", "high"] },
1413
+ rest_level: { type: "string", enum: ["low", "medium", "high"] }
1414
+ },
1415
+ additionalProperties: true
1416
+ },
1417
+ host: { type: "string" },
1418
+ port: { type: "integer", minimum: 1 },
1419
+ slow_live: { type: "boolean" },
1420
+ delay_ms: { type: "integer", minimum: 0 },
1421
+ detail_limit: { type: "integer", minimum: 0 },
1422
+ execute_post_action: { type: "boolean" },
1423
+ no_filter: { type: "boolean" },
1424
+ dry_run: { type: "boolean" }
1425
+ },
1426
+ required: ["instruction"],
1427
+ additionalProperties: true
1428
+ };
1429
+ }
1430
+
1431
+ function createCompactScheduleRunInputSchema() {
1432
+ const base = createCompactRunInputSchema();
1433
+ return {
1434
+ ...base,
1435
+ properties: {
1436
+ ...base.properties,
1437
+ schedule_id: {
1438
+ type: "string",
1439
+ description: "可选,自定义定时任务 id;默认自动生成"
1440
+ },
1441
+ schedule_run_at: {
1442
+ type: "string",
1443
+ description: "ISO 时间字符串"
1444
+ },
1445
+ schedule_delay_minutes: {
1446
+ type: "number",
1447
+ minimum: 0
1448
+ },
1449
+ schedule_delay_seconds: {
1450
+ type: "number",
1451
+ minimum: 0
1452
+ }
1453
+ }
1454
+ };
1455
+ }
1456
+
1457
+ function normalizeMcpToolset(value) {
1458
+ const raw = String(value || "").trim().toLowerCase().replace(/[\s_]+/g, "-");
1459
+ if (!raw) return MCP_TOOLSET_ALL;
1460
+ if (raw === "boss-recommend" || raw === "recommend-page" || raw === "recommended") return MCP_TOOLSET_RECOMMEND;
1461
+ if (raw === "boss-chat" || raw === "chat-only" || raw === "chat-page") return MCP_TOOLSET_CHAT;
1462
+ if (raw === "boss-recruit" || raw === "search" || raw === "search-page" || raw === "recruit-page") return MCP_TOOLSET_RECRUIT;
1463
+ return VALID_MCP_TOOLSETS.has(raw) ? raw : MCP_TOOLSET_ALL;
1464
+ }
1465
+
1466
+ function getConfiguredMcpToolset(argv = process.argv, env = process.env) {
1467
+ const args = Array.isArray(argv) ? argv.slice(2) : [];
1468
+ for (let index = 0; index < args.length; index += 1) {
1469
+ const arg = String(args[index] || "");
1470
+ if (arg === "--toolset" || arg === "--tools") {
1471
+ return normalizeMcpToolset(args[index + 1]);
1472
+ }
1473
+ if (arg.startsWith("--toolset=")) {
1474
+ return normalizeMcpToolset(arg.slice("--toolset=".length));
1475
+ }
1476
+ if (arg.startsWith("--tools=")) {
1477
+ return normalizeMcpToolset(arg.slice("--tools=".length));
1478
+ }
1479
+ }
1480
+ return normalizeMcpToolset(env?.[MCP_TOOLSET_ENV]);
1481
+ }
1482
+
1483
+ function toolNamesForMcpToolset(toolset) {
1484
+ const normalized = normalizeMcpToolset(toolset);
1485
+ if (normalized === MCP_TOOLSET_RECOMMEND) return RECOMMEND_TOOL_NAMES;
1486
+ if (normalized === MCP_TOOLSET_CHAT) return BOSS_CHAT_TOOL_NAMES;
1487
+ if (normalized === MCP_TOOLSET_RECRUIT) return RECRUIT_TOOL_NAMES;
1488
+ return null;
1489
+ }
1490
+
1491
+ function filterToolsForMcpToolset(tools, toolset = getConfiguredMcpToolset()) {
1492
+ const names = toolNamesForMcpToolset(toolset);
1493
+ if (!names) return tools;
1494
+ return tools.filter((tool) => names.has(tool.name));
1495
+ }
1496
+
1497
+ function createToolsSchema(toolset = getConfiguredMcpToolset()) {
1498
+ const normalizedToolset = normalizeMcpToolset(toolset);
1499
+ const runInputSchema = normalizedToolset === MCP_TOOLSET_RECOMMEND
1500
+ ? createCompactRunInputSchema()
1501
+ : createRunInputSchema();
1502
+ const scheduleRunInputSchema = normalizedToolset === MCP_TOOLSET_RECOMMEND
1503
+ ? createCompactScheduleRunInputSchema()
1504
+ : createScheduleRunInputSchema();
1505
+ const tools = [
1506
+ {
1507
+ name: TOOL_BOSS_CHAT_HEALTH_CHECK,
1508
+ description: "Boss 聊天页/chat-only 健康检查。chat-only、未读、全部聊天、求简历等任务必须先走 boss-chat 工具,不要调用 list_recommend_jobs 或 start_recommend_pipeline_run。",
1509
+ inputSchema: {
1510
+ type: "object",
1511
+ properties: {
1512
+ host: {
1513
+ type: "string",
1514
+ description: "可选,Chrome 调试 host;默认 127.0.0.1"
1515
+ },
1516
+ port: {
1517
+ type: "integer",
1518
+ minimum: 1,
1519
+ description: "可选,Chrome 调试端口;默认读取 screening-config.json.debugPort 或 9222"
1520
+ },
1521
+ target_url_includes: {
1522
+ type: "string",
1523
+ description: "可选,Chrome target URL 匹配片段;默认 Boss chat 页"
1524
+ },
1525
+ allow_navigate: {
1526
+ type: "boolean",
1527
+ description: "可选,未在 chat 页时允许通过 Page.navigate 切换;默认 true"
1528
+ },
1529
+ slow_live: {
1530
+ type: "boolean",
1531
+ description: "可选,VPN/慢页面 live 测试模式,放宽等待时间"
1532
+ }
1533
+ },
1534
+ additionalProperties: false
1535
+ }
1536
+ },
1537
+ {
1538
+ name: TOOL_BOSS_CHAT_LIST_JOBS,
1539
+ description: "只读读取 Boss 聊天页岗位列表;这是 chat-only 获取 job_options 的首选别名,等价于 prepare_boss_chat_run 的预备步骤但不会启动任务。聊天页/未读/全部聊天/求简历任务必须用本工具或 prepare_boss_chat_run,严禁用 list_recommend_jobs。",
1540
+ inputSchema: createBossChatStartInputSchema()
1541
+ },
1542
+ {
1543
+ name: TOOL_BOSS_CHAT_PREPARE_RUN,
1544
+ description: "预备一次 boss-chat/chat-only 任务:只导航聊天页并返回岗位列表与待补字段,不会启动任务。用它先获取 job_options;不要用 list_recommend_jobs。",
1545
+ inputSchema: createBossChatStartInputSchema()
1546
+ },
1547
+ {
1548
+ name: TOOL_BOSS_CHAT_START_RUN,
1549
+ description: "异步启动一次 boss-chat/chat-only 任务。必须一次性提供 job、start_from、target_count、criteria;若用户选择扫到底/不限/全部候选人,必须字面传 target_count=\"all\"。严禁改用 start_recommend_pipeline_run。",
1550
+ inputSchema: createBossChatStartInputSchema({ requireFullInput: true })
1551
+ },
1552
+ {
1553
+ name: TOOL_BOSS_CHAT_GET_RUN,
1554
+ description: "查询 boss-chat run_id 的当前状态。chat-only 状态查询用本工具,不要用 get_recommend_pipeline_run。",
1555
+ inputSchema: {
1556
+ type: "object",
1557
+ properties: {
1558
+ run_id: { type: "string" },
1559
+ profile: { type: "string" }
1560
+ },
1561
+ required: ["run_id"],
1562
+ additionalProperties: false
1563
+ }
1564
+ },
1565
+ {
1566
+ name: TOOL_BOSS_CHAT_PAUSE_RUN,
1567
+ description: "暂停运行中的 boss-chat 任务。",
1568
+ inputSchema: {
1569
+ type: "object",
1570
+ properties: {
1571
+ run_id: { type: "string" },
1572
+ profile: { type: "string" }
1573
+ },
1574
+ required: ["run_id"],
1575
+ additionalProperties: false
1576
+ }
1577
+ },
1578
+ {
1579
+ name: TOOL_BOSS_CHAT_RESUME_RUN,
1580
+ description: "继续已暂停的 boss-chat 任务。",
1581
+ inputSchema: {
1582
+ type: "object",
1583
+ properties: {
1584
+ run_id: { type: "string" },
1585
+ profile: { type: "string" }
1586
+ },
1587
+ required: ["run_id"],
1588
+ additionalProperties: false
1589
+ }
1590
+ },
1591
+ {
1592
+ name: TOOL_BOSS_CHAT_CANCEL_RUN,
1593
+ description: "取消运行中的 boss-chat 任务。",
1594
+ inputSchema: {
1595
+ type: "object",
1596
+ properties: {
1597
+ run_id: { type: "string" },
1598
+ profile: { type: "string" }
1599
+ },
1600
+ required: ["run_id"],
1601
+ additionalProperties: false
1602
+ }
1603
+ },
1298
1604
  {
1299
1605
  name: TOOL_LIST_RECOMMEND_JOBS,
1300
- description: "CDP-only 读取 Boss 推荐页岗位下拉框,返回所有可用岗位完整名称,方便 cron/一次性任务提前填写 job 参数。不会启动筛选任务。",
1606
+ description: "CDP-only 读取 Boss 推荐页岗位下拉框,返回所有可用岗位完整名称,方便 recommend/推荐页 cron/一次性任务提前填写 job 参数。不会启动筛选任务。chat-only、未读、全部聊天、求简历任务严禁调用本工具,必须用 list_boss_chat_jobs 或 prepare_boss_chat_run。",
1301
1607
  inputSchema: createListRecommendJobsInputSchema()
1302
1608
  },
1303
1609
  {
1304
1610
  name: TOOL_RUN_RECOMMEND,
1305
1611
  description: "立即通过原生 MCP 启动 Boss 推荐页筛选。它是 start_recommend_pipeline_run 的短别名;用户已经确认并要现在启动时,优先调用本工具,不需要先调用 prepare_recommend_pipeline_run。必须作为 MCP tool call 调用,禁止通过 terminal/shell/run_command/PowerShell/CLI/manual JSON-RPC 代替,也不要用 schedule_recommend_pipeline_run 冒充立即启动。",
1306
- inputSchema: createRunInputSchema()
1612
+ inputSchema: runInputSchema
1307
1613
  },
1308
1614
  {
1309
1615
  name: TOOL_START_RUN,
1310
1616
  description: "立即通过原生 MCP 异步启动 Boss 推荐页流水线(含同步门禁预检)。用户已经确认并要现在启动时,优先调用本工具或 run_recommend,不需要先调用 prepare_recommend_pipeline_run。必须作为 MCP tool call 调用,禁止通过 terminal/shell/run_command/PowerShell/CLI/manual JSON-RPC 代替,也不要用 schedule_recommend_pipeline_run 冒充立即启动。",
1311
- inputSchema: createRunInputSchema()
1617
+ inputSchema: runInputSchema
1312
1618
  },
1313
1619
  {
1314
1620
  name: TOOL_PREPARE_RUN,
1315
1621
  description: "只校验 Boss 推荐页流水线参数是否完整;不会启动筛选任务。主要用于显式预检或稍后/cron/定时启动前校验。若用户要现在运行,READY/cron_ready=true 后必须继续调用本 MCP server 的 run_recommend 或 start_recommend_pipeline_run;prepare 能返回结果就证明原生 MCP 可用,禁止改用 terminal/shell/run_command/PowerShell/CLI/manual JSON-RPC,也禁止再次调用 prepare 试图启动。",
1316
- inputSchema: createRunInputSchema()
1622
+ inputSchema: runInputSchema
1317
1623
  },
1318
1624
  {
1319
1625
  name: TOOL_SCHEDULE_RUN,
1320
1626
  description: "只用于用户明确要求稍后/cron/定时启动的 package-owned Boss 推荐页定时任务。若用户要现在运行,必须调用 run_recommend 或 start_recommend_pipeline_run,不要用短延迟 schedule 冒充立即启动。schedule 会先校验 READY/cron_ready,再保存完整 payload,并由 detached scheduler 到点直接启动,不依赖 AI harness 自己拼 shell cron。",
1321
- inputSchema: createScheduleRunInputSchema()
1627
+ inputSchema: scheduleRunInputSchema
1322
1628
  },
1323
1629
  {
1324
1630
  name: TOOL_GET_SCHEDULED_RUN,
@@ -1428,99 +1734,6 @@ function createToolsSchema() {
1428
1734
  description: "手动运维自愈工具:扫描 Boss recommend 相关 selector / network 规则漂移,并在确认后应用高置信度修复。",
1429
1735
  inputSchema: createRunRecommendSelfHealInputSchema()
1430
1736
  },
1431
- {
1432
- name: TOOL_BOSS_CHAT_HEALTH_CHECK,
1433
- description: "CDP-only 检查 Boss chat 页面、自愈 probes、共享 screening-config.json 与 chat runtime 目录是否可用。",
1434
- inputSchema: {
1435
- type: "object",
1436
- properties: {
1437
- host: {
1438
- type: "string",
1439
- description: "可选,Chrome 调试 host;默认 127.0.0.1"
1440
- },
1441
- port: {
1442
- type: "integer",
1443
- minimum: 1,
1444
- description: "可选,Chrome 调试端口;默认读取 screening-config.json.debugPort 或 9222"
1445
- },
1446
- target_url_includes: {
1447
- type: "string",
1448
- description: "可选,Chrome target URL 匹配片段;默认 Boss chat 页"
1449
- },
1450
- allow_navigate: {
1451
- type: "boolean",
1452
- description: "可选,未在 chat 页时允许通过 Page.navigate 切换;默认 true"
1453
- },
1454
- slow_live: {
1455
- type: "boolean",
1456
- description: "可选,VPN/慢页面 live 测试模式,放宽等待时间"
1457
- }
1458
- },
1459
- additionalProperties: false
1460
- }
1461
- },
1462
- {
1463
- name: TOOL_BOSS_CHAT_PREPARE_RUN,
1464
- description: "预备一次 boss-chat 任务:只导航聊天页并返回岗位列表与待补字段,不会启动任务。用它先获取 job_options。",
1465
- inputSchema: createBossChatStartInputSchema()
1466
- },
1467
- {
1468
- name: TOOL_BOSS_CHAT_START_RUN,
1469
- description: "异步启动一次 boss-chat 任务。必须一次性提供 job、start_from、target_count、criteria;若用户选择扫到底/不限/全部候选人,必须字面传 target_count=\"all\"。",
1470
- inputSchema: createBossChatStartInputSchema({ requireFullInput: true })
1471
- },
1472
- {
1473
- name: TOOL_BOSS_CHAT_GET_RUN,
1474
- description: "查询 boss-chat run_id 的当前状态。",
1475
- inputSchema: {
1476
- type: "object",
1477
- properties: {
1478
- run_id: { type: "string" },
1479
- profile: { type: "string" }
1480
- },
1481
- required: ["run_id"],
1482
- additionalProperties: false
1483
- }
1484
- },
1485
- {
1486
- name: TOOL_BOSS_CHAT_PAUSE_RUN,
1487
- description: "暂停运行中的 boss-chat 任务。",
1488
- inputSchema: {
1489
- type: "object",
1490
- properties: {
1491
- run_id: { type: "string" },
1492
- profile: { type: "string" }
1493
- },
1494
- required: ["run_id"],
1495
- additionalProperties: false
1496
- }
1497
- },
1498
- {
1499
- name: TOOL_BOSS_CHAT_RESUME_RUN,
1500
- description: "继续已暂停的 boss-chat 任务。",
1501
- inputSchema: {
1502
- type: "object",
1503
- properties: {
1504
- run_id: { type: "string" },
1505
- profile: { type: "string" }
1506
- },
1507
- required: ["run_id"],
1508
- additionalProperties: false
1509
- }
1510
- },
1511
- {
1512
- name: TOOL_BOSS_CHAT_CANCEL_RUN,
1513
- description: "取消运行中的 boss-chat 任务。",
1514
- inputSchema: {
1515
- type: "object",
1516
- properties: {
1517
- run_id: { type: "string" },
1518
- profile: { type: "string" }
1519
- },
1520
- required: ["run_id"],
1521
- additionalProperties: false
1522
- }
1523
- },
1524
1737
  {
1525
1738
  name: TOOL_RUN_RECRUIT_PIPELINE,
1526
1739
  description: "兼容 Boss recruit 入口:默认 async;sync 模式会等待终态。所有浏览器动作走共享 CDP-only recruit service。",
@@ -1552,6 +1765,7 @@ function createToolsSchema() {
1552
1765
  inputSchema: createRecruitRunIdInputSchema()
1553
1766
  }
1554
1767
  ];
1768
+ return filterToolsForMcpToolset(tools, toolset);
1555
1769
  }
1556
1770
 
1557
1771
  function createToolResultResponse(id, payload, isError = false) {
@@ -2853,10 +3067,19 @@ async function handleRequest(message, workspaceRoot) {
2853
3067
  };
2854
3068
  }
2855
3069
 
2856
- if (method === "tools/call") {
2857
- const toolName = params?.name;
2858
- const args = params?.arguments || {};
2859
-
3070
+ if (method === "tools/call") {
3071
+ const toolName = params?.name;
3072
+ const args = params?.arguments || {};
3073
+ const toolset = getConfiguredMcpToolset();
3074
+ const visibleToolNames = new Set(createToolsSchema(toolset).map((tool) => tool.name));
3075
+ if (!visibleToolNames.has(toolName)) {
3076
+ return createJsonRpcError(
3077
+ id,
3078
+ -32602,
3079
+ `Tool ${toolName || ""} is not available in the ${toolset} boss-recommend-mcp toolset. Use the MCP server/toolset that exposes this domain instead of terminal or CLI fallback.`
3080
+ );
3081
+ }
3082
+
2860
3083
  if ([TOOL_RUN_RECOMMEND, TOOL_START_RUN].includes(toolName)) {
2861
3084
  const inputError = validateRunArgs(args);
2862
3085
  if (inputError) {
@@ -2885,7 +3108,7 @@ async function handleRequest(message, workspaceRoot) {
2885
3108
  }
2886
3109
  }
2887
3110
 
2888
- if ([TOOL_BOSS_CHAT_PREPARE_RUN, TOOL_BOSS_CHAT_START_RUN].includes(toolName)) {
3111
+ if ([TOOL_BOSS_CHAT_LIST_JOBS, TOOL_BOSS_CHAT_PREPARE_RUN, TOOL_BOSS_CHAT_START_RUN].includes(toolName)) {
2889
3112
  const inputError = validateBossChatStartArgs(args);
2890
3113
  if (inputError) {
2891
3114
  return createJsonRpcError(id, -32602, inputError);
@@ -2941,6 +3164,8 @@ async function handleRequest(message, workspaceRoot) {
2941
3164
  payload = await handleRunRecommendSelfHealTool({ workspaceRoot, args });
2942
3165
  } else if (toolName === TOOL_BOSS_CHAT_HEALTH_CHECK) {
2943
3166
  payload = await handleBossChatHealthCheckTool(workspaceRoot, args);
3167
+ } else if (toolName === TOOL_BOSS_CHAT_LIST_JOBS) {
3168
+ payload = await handleBossChatPrepareRunTool({ workspaceRoot, args });
2944
3169
  } else if (toolName === TOOL_BOSS_CHAT_PREPARE_RUN) {
2945
3170
  payload = await handleBossChatPrepareRunTool({ workspaceRoot, args });
2946
3171
  } else if (toolName === TOOL_BOSS_CHAT_START_RUN) {
@@ -3097,6 +3322,9 @@ export function startServer() {
3097
3322
  }
3098
3323
 
3099
3324
  export const __testables = {
3325
+ createToolsSchema,
3326
+ getConfiguredMcpToolset,
3327
+ normalizeMcpToolset,
3100
3328
  handleRequest,
3101
3329
  runDetachedWorkerForTests(options = {}) {
3102
3330
  return runDetachedWorker(options);
@@ -75,13 +75,119 @@ let recommendRunService = createRecommendRunService({
75
75
  });
76
76
  const recommendRunMeta = new Map();
77
77
 
78
- function normalizeText(value) {
79
- return String(value || "").replace(/\s+/g, " ").trim();
80
- }
81
-
82
- function parsePositiveInteger(raw, fallback) {
83
- const parsed = Number.parseInt(String(raw || ""), 10);
84
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
78
+ function normalizeText(value) {
79
+ return String(value || "").replace(/\s+/g, " ").trim();
80
+ }
81
+
82
+ function collectRecommendRouteText(args = {}) {
83
+ return normalizeText([
84
+ args.instruction,
85
+ args.criteria,
86
+ args.target_url_includes,
87
+ args.page_scope,
88
+ args.confirmation?.page_value,
89
+ args.confirmation?.job_value,
90
+ args.overrides?.criteria,
91
+ args.overrides?.page_scope,
92
+ args.overrides?.job,
93
+ args.overrides?.target_url_includes
94
+ ].filter((value) => value !== undefined && value !== null).join(" "));
95
+ }
96
+
97
+ function findRouteSignals(text, patterns = []) {
98
+ const signals = [];
99
+ for (const { label, pattern } of patterns) {
100
+ if (pattern.test(text)) signals.push(label);
101
+ }
102
+ return signals;
103
+ }
104
+
105
+ function detectNonRecommendRoute(args = {}) {
106
+ const text = collectRecommendRouteText(args);
107
+ if (!text) return null;
108
+ const chatSignals = findRouteSignals(text, [
109
+ { label: "chat-only", pattern: /\bchat[-\s]?only\b/i },
110
+ { label: "boss-chat", pattern: /\bboss[-\s]?chat\b/i },
111
+ { label: "chat page", pattern: /\bchat\s+page\b/i },
112
+ { label: "chat/index", pattern: /(?:\/web\/chat\/index|chat\/index)/i },
113
+ { label: "聊天页", pattern: /聊天页/ },
114
+ { label: "聊天列表", pattern: /聊天列表/ },
115
+ { label: "未读", pattern: /未读/ },
116
+ { label: "全部聊天", pattern: /全部聊天|所有聊天/ },
117
+ { label: "求简历", pattern: /求简历|索要简历|要简历|在线简历/ }
118
+ ]);
119
+ if (chatSignals.length) {
120
+ return {
121
+ domain: "chat",
122
+ signals: chatSignals,
123
+ text
124
+ };
125
+ }
126
+ const searchSignals = findRouteSignals(text, [
127
+ { label: "search-only", pattern: /\bsearch[-\s]?only\b/i },
128
+ { label: "search page", pattern: /\bsearch\s+page\b/i },
129
+ { label: "recruit pipeline", pattern: /\brecruit\s+pipeline\b/i },
130
+ { label: "chat/search", pattern: /(?:\/web\/chat\/search|chat\/search)/i },
131
+ { label: "搜索页", pattern: /搜索页/ },
132
+ { label: "招聘搜索", pattern: /招聘搜索/ }
133
+ ]);
134
+ if (searchSignals.length) {
135
+ return {
136
+ domain: "search",
137
+ signals: searchSignals,
138
+ text
139
+ };
140
+ }
141
+ return null;
142
+ }
143
+
144
+ function buildWrongRecommendRouteResponse(route) {
145
+ if (route?.domain === "chat") {
146
+ return {
147
+ status: "FAILED",
148
+ route_guard: true,
149
+ error: {
150
+ code: "WRONG_BOSS_TOOL_FOR_CHAT",
151
+ message: "This request is explicitly chat-only/chat-page work. Do not use recommend tools. Use boss_chat_health_check, then list_boss_chat_jobs or prepare_boss_chat_run, then start_boss_chat_run.",
152
+ retryable: false
153
+ },
154
+ detected_domain: "chat",
155
+ detected_signals: route.signals || [],
156
+ recommended_tool_sequence: [
157
+ "boss_chat_health_check",
158
+ "list_boss_chat_jobs",
159
+ "prepare_boss_chat_run",
160
+ "start_boss_chat_run"
161
+ ]
162
+ };
163
+ }
164
+ if (route?.domain === "search") {
165
+ return {
166
+ status: "FAILED",
167
+ route_guard: true,
168
+ error: {
169
+ code: "WRONG_BOSS_TOOL_FOR_SEARCH",
170
+ message: "This request is explicitly search/recruit-page work. Do not use recommend tools. Use run_recruit_pipeline or start_recruit_pipeline_run.",
171
+ retryable: false
172
+ },
173
+ detected_domain: "search",
174
+ detected_signals: route.signals || [],
175
+ recommended_tool_sequence: [
176
+ "run_recruit_pipeline",
177
+ "start_recruit_pipeline_run"
178
+ ]
179
+ };
180
+ }
181
+ return null;
182
+ }
183
+
184
+ function guardRecommendRoute(args = {}) {
185
+ return buildWrongRecommendRouteResponse(detectNonRecommendRoute(args));
186
+ }
187
+
188
+ function parsePositiveInteger(raw, fallback) {
189
+ const parsed = Number.parseInt(String(raw || ""), 10);
190
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
85
191
  }
86
192
 
87
193
  function parseNonNegativeInteger(raw, fallback) {
@@ -855,9 +961,17 @@ async function readRecommendJobOptionsFromSession(session) {
855
961
  };
856
962
  }
857
963
 
858
- export async function listRecommendJobsTool({ workspaceRoot = "", args = {} } = {}) {
859
- const configResolution = resolveBossScreeningConfig(workspaceRoot);
860
- const host = normalizeText(args.host) || DEFAULT_RECOMMEND_HOST;
964
+ export async function listRecommendJobsTool({ workspaceRoot = "", args = {} } = {}) {
965
+ const routeGuard = guardRecommendRoute(args);
966
+ if (routeGuard) {
967
+ return {
968
+ ...routeGuard,
969
+ stage: "recommend_job_list",
970
+ message: "list_recommend_jobs is recommend-page only. For chat-only job lists, call list_boss_chat_jobs or prepare_boss_chat_run."
971
+ };
972
+ }
973
+ const configResolution = resolveBossScreeningConfig(workspaceRoot);
974
+ const host = normalizeText(args.host) || DEFAULT_RECOMMEND_HOST;
861
975
  const port = parsePositiveInteger(
862
976
  args.port,
863
977
  configResolution.ok ? configResolution.config.debugPort : DEFAULT_RECOMMEND_PORT
@@ -1477,8 +1591,10 @@ function getRunOptions(args, parsed, normalized, session, configResolution = nul
1477
1591
  };
1478
1592
  }
1479
1593
 
1480
- function prepareRecommendPipelineStart(args = {}, { workspaceRoot = "" } = {}) {
1481
- const parsed = parseRecommendPipelineRequest(args);
1594
+ function prepareRecommendPipelineStart(args = {}, { workspaceRoot = "" } = {}) {
1595
+ const routeGuard = guardRecommendRoute(args);
1596
+ if (routeGuard) return { response: routeGuard };
1597
+ const parsed = parseRecommendPipelineRequest(args);
1482
1598
  const gate = evaluateRecommendPipelineGate(parsed, args);
1483
1599
  if (gate) return { response: gate };
1484
1600
  const configResolution = resolveBossScreeningConfig(workspaceRoot);