@reconcrap/boss-recommend-mcp 2.1.11 → 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
@@ -225,8 +225,9 @@ node src/cli.js start
225
225
  如果检测到 legacy Boss server entries,installer 会:
226
226
 
227
227
  - 保留非 Boss MCP server。
228
- - 默认写入统一 server:`boss-recommend -> npx -y @reconcrap/boss-recommend-mcp@<installed-version> start`
229
- - 如果传入 `--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` 持续升级。
230
231
  - 从同一个 `mcp.json` 删除旧 `boss-recruit-mcp`、standalone `boss-chat`、旧本地 Boss repo 路径,避免 agent 继续调用 legacy 包。
231
232
  - 在原文件旁生成 `mcp.json.boss-mcp-migration-*.bak`。
232
233
  - 同步外部 skills 目录里的 `boss-recommend-pipeline`、`boss-recruit-pipeline`、`boss-chat`。
@@ -263,6 +264,7 @@ BOSS_RECOMMEND_EXTERNAL_SKILL_DIRS="/path/to/skills" boss-recommend-mcp install
263
264
  BOSS_RECOMMEND_HOME # 统一状态目录,默认 ~/.boss-recommend-mcp
264
265
  BOSS_CHAT_HOME # 覆盖 boss-chat 运行态目录;默认 ~/.boss-recommend-mcp/boss-chat
265
266
  BOSS_RECOMMEND_SCREEN_CONFIG # 显式指定 screening-config.json 路径(最高优先级)
267
+ BOSS_RECOMMEND_MCP_TOOLSET # 可选收窄 MCP 工具:all|recommend|chat|recruit;Trae/Trae-CN installer 会自动设置
266
268
  BOSS_RECOMMEND_MCP_CONFIG_TARGETS # JSON 数组或系统 path 分隔路径列表,指定额外 mcp.json 目标文件
267
269
  BOSS_RECOMMEND_EXTERNAL_SKILL_DIRS # JSON 数组或系统 path 分隔路径列表,指定额外 skills 根目录
268
270
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "2.1.11",
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",
@@ -13,6 +13,8 @@ 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`
17
19
  - 只读获取聊天页岗位列表:`list_boss_chat_jobs`
18
20
  - 预备并获取岗位列表:`prepare_boss_chat_run`
@@ -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
@@ -123,8 +123,50 @@ const TOOL_GET_RECRUIT_PIPELINE_RUN = "get_recruit_pipeline_run";
123
123
  const TOOL_CANCEL_RECRUIT_PIPELINE_RUN = "cancel_recruit_pipeline_run";
124
124
  const TOOL_PAUSE_RECRUIT_PIPELINE_RUN = "pause_recruit_pipeline_run";
125
125
  const TOOL_RESUME_RECRUIT_PIPELINE_RUN = "resume_recruit_pipeline_run";
126
-
127
- 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
+ ]);
128
170
  const FRAMING_UNKNOWN = "unknown";
129
171
  const FRAMING_HEADER = "header";
130
172
  const FRAMING_LINE = "line";
@@ -454,6 +496,26 @@ function readRawRunState(runId) {
454
496
  }
455
497
  }
456
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
+
457
519
  function getRunSortTime(run = {}, fallbackMs = 0) {
458
520
  for (const key of ["updated_at", "heartbeat_at", "completed_at", "started_at", "updatedAt", "completedAt", "startedAt"]) {
459
521
  const ms = Date.parse(run?.[key] || "");
@@ -477,7 +539,7 @@ function compactRunForList(run = {}) {
477
539
  heartbeat_at: run.heartbeat_at || null,
478
540
  completed_at: run.completed_at || run.completedAt || null,
479
541
  pid: Number.isInteger(run.pid) && run.pid > 0 ? run.pid : null,
480
- progress: run.progress || {},
542
+ progress: compactProgressForList(run.progress),
481
543
  last_message: normalizeText(run.last_message || error?.message || ""),
482
544
  control: {
483
545
  pause_requested: run.control?.pause_requested === true,
@@ -494,11 +556,10 @@ function compactRunForList(run = {}) {
494
556
  report_json: normalizeText(result.report_json || result.result?.report_json || ""),
495
557
  checkpoint_path: normalizeText(result.checkpoint_path || result.result?.checkpoint_path || "")
496
558
  } : null,
497
- resume: {
498
- checkpoint_path: normalizeText(run.resume?.checkpoint_path || ""),
499
- output_csv: normalizeText(run.resume?.output_csv || ""),
500
- worker_stdout_path: normalizeText(run.resume?.worker_stdout_path || ""),
501
- 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 || "")
502
563
  }
503
564
  };
504
565
  }
@@ -1294,8 +1355,154 @@ function createScheduleRunInputSchema() {
1294
1355
  };
1295
1356
  }
1296
1357
 
1297
- function createToolsSchema() {
1298
- 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 = [
1299
1506
  {
1300
1507
  name: TOOL_BOSS_CHAT_HEALTH_CHECK,
1301
1508
  description: "Boss 聊天页/chat-only 健康检查。chat-only、未读、全部聊天、求简历等任务必须先走 boss-chat 工具,不要调用 list_recommend_jobs 或 start_recommend_pipeline_run。",
@@ -1402,22 +1609,22 @@ function createToolsSchema() {
1402
1609
  {
1403
1610
  name: TOOL_RUN_RECOMMEND,
1404
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 冒充立即启动。",
1405
- inputSchema: createRunInputSchema()
1612
+ inputSchema: runInputSchema
1406
1613
  },
1407
1614
  {
1408
1615
  name: TOOL_START_RUN,
1409
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 冒充立即启动。",
1410
- inputSchema: createRunInputSchema()
1617
+ inputSchema: runInputSchema
1411
1618
  },
1412
1619
  {
1413
1620
  name: TOOL_PREPARE_RUN,
1414
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 试图启动。",
1415
- inputSchema: createRunInputSchema()
1622
+ inputSchema: runInputSchema
1416
1623
  },
1417
1624
  {
1418
1625
  name: TOOL_SCHEDULE_RUN,
1419
1626
  description: "只用于用户明确要求稍后/cron/定时启动的 package-owned Boss 推荐页定时任务。若用户要现在运行,必须调用 run_recommend 或 start_recommend_pipeline_run,不要用短延迟 schedule 冒充立即启动。schedule 会先校验 READY/cron_ready,再保存完整 payload,并由 detached scheduler 到点直接启动,不依赖 AI harness 自己拼 shell cron。",
1420
- inputSchema: createScheduleRunInputSchema()
1627
+ inputSchema: scheduleRunInputSchema
1421
1628
  },
1422
1629
  {
1423
1630
  name: TOOL_GET_SCHEDULED_RUN,
@@ -1558,6 +1765,7 @@ function createToolsSchema() {
1558
1765
  inputSchema: createRecruitRunIdInputSchema()
1559
1766
  }
1560
1767
  ];
1768
+ return filterToolsForMcpToolset(tools, toolset);
1561
1769
  }
1562
1770
 
1563
1771
  function createToolResultResponse(id, payload, isError = false) {
@@ -2859,10 +3067,19 @@ async function handleRequest(message, workspaceRoot) {
2859
3067
  };
2860
3068
  }
2861
3069
 
2862
- if (method === "tools/call") {
2863
- const toolName = params?.name;
2864
- const args = params?.arguments || {};
2865
-
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
+
2866
3083
  if ([TOOL_RUN_RECOMMEND, TOOL_START_RUN].includes(toolName)) {
2867
3084
  const inputError = validateRunArgs(args);
2868
3085
  if (inputError) {
@@ -3105,6 +3322,9 @@ export function startServer() {
3105
3322
  }
3106
3323
 
3107
3324
  export const __testables = {
3325
+ createToolsSchema,
3326
+ getConfiguredMcpToolset,
3327
+ normalizeMcpToolset,
3108
3328
  handleRequest,
3109
3329
  runDetachedWorkerForTests(options = {}) {
3110
3330
  return runDetachedWorker(options);