@reconcrap/boss-recruit-mcp 1.0.19 → 1.0.21

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/src/index.js CHANGED
@@ -2,16 +2,59 @@ import path from "node:path";
2
2
  import { createRequire } from "node:module";
3
3
  import process from "node:process";
4
4
  import { fileURLToPath } from "node:url";
5
- import { runRecruitPipeline } from "./pipeline.js";
5
+ import {
6
+ PIPELINE_STATUS_READY_TO_START_ASYNC,
7
+ runRecruitPipeline
8
+ } from "./pipeline.js";
9
+ import {
10
+ RUN_MODE_ASYNC,
11
+ RUN_MODE_SYNC,
12
+ RUN_STAGE_PREFLIGHT,
13
+ RUN_STATE_CANCELED,
14
+ RUN_STATE_COMPLETED,
15
+ RUN_STATE_FAILED,
16
+ RUN_STATE_RUNNING,
17
+ cleanupExpiredRuns,
18
+ createRunId,
19
+ createRunStateSnapshot,
20
+ getRunHeartbeatIntervalMs,
21
+ readRunState,
22
+ touchRunHeartbeat,
23
+ updateRunProgress,
24
+ updateRunState,
25
+ writeRunState
26
+ } from "./run-state.js";
6
27
 
7
28
  const require = createRequire(import.meta.url);
8
29
  const { version: SERVER_VERSION } = require("../package.json");
9
- const TOOL_NAME = "run_recruit_pipeline";
30
+
31
+ const TOOL_RUN_PIPELINE = "run_recruit_pipeline";
32
+ const TOOL_START_RUN = "start_recruit_pipeline_run";
33
+ const TOOL_GET_RUN = "get_recruit_pipeline_run";
34
+ const TOOL_CANCEL_RUN = "cancel_recruit_pipeline_run";
35
+
10
36
  const SERVER_NAME = "boss-recruit-mcp";
11
37
  const FRAMING_UNKNOWN = "unknown";
12
38
  const FRAMING_HEADER = "header";
13
39
  const FRAMING_LINE = "line";
14
40
 
41
+ const activeAsyncRuns = new Map();
42
+ let runPipelineImpl = runRecruitPipeline;
43
+
44
+ function normalizeText(value) {
45
+ return String(value || "").replace(/\s+/g, " ").trim();
46
+ }
47
+
48
+ function parsePositiveInteger(raw, fallback) {
49
+ const value = Number.parseInt(String(raw || ""), 10);
50
+ return Number.isFinite(value) && value > 0 ? value : fallback;
51
+ }
52
+
53
+ function getDefaultPollAfterSec() {
54
+ const fromEnv = parsePositiveInteger(process.env.BOSS_RECRUIT_POLL_AFTER_SEC, 10);
55
+ return Math.max(5, Math.min(15, fromEnv));
56
+ }
57
+
15
58
  function writeMessage(message, framing = FRAMING_LINE) {
16
59
  const body = JSON.stringify(message);
17
60
  if (framing === FRAMING_HEADER) {
@@ -30,48 +73,530 @@ function createJsonRpcError(id, code, message) {
30
73
  };
31
74
  }
32
75
 
33
- function createToolSchema() {
76
+ function createRunInputSchema() {
34
77
  return {
35
- name: TOOL_NAME,
36
- description: "统一招聘流水线:解析招聘指令、校验条件、执行搜索与筛选并返回摘要。",
37
- inputSchema: {
38
- type: "object",
39
- properties: {
40
- instruction: {
41
- type: "string",
42
- description: "用户自然语言招聘指令"
78
+ type: "object",
79
+ properties: {
80
+ instruction: {
81
+ type: "string",
82
+ description: "用户自然语言招聘指令"
83
+ },
84
+ execution_mode: {
85
+ type: "string",
86
+ enum: [RUN_MODE_ASYNC, RUN_MODE_SYNC],
87
+ description: "执行模式;默认 async。"
88
+ },
89
+ confirmation: {
90
+ type: "object",
91
+ properties: {
92
+ keyword_confirmed: { type: "boolean" },
93
+ keyword_value: { type: "string" },
94
+ search_params_confirmed: { type: "boolean" },
95
+ criteria_confirmed: { type: "boolean" },
96
+ use_default_for_missing: { type: "boolean" }
43
97
  },
44
- confirmation: {
45
- type: "object",
46
- properties: {
47
- keyword_confirmed: { type: "boolean" },
48
- keyword_value: { type: "string" },
49
- search_params_confirmed: { type: "boolean" },
50
- use_default_for_missing: { type: "boolean" }
98
+ additionalProperties: false
99
+ },
100
+ overrides: {
101
+ type: "object",
102
+ properties: {
103
+ city: { type: "string" },
104
+ degree: { type: "string" },
105
+ filter_recent_viewed: { type: "boolean" },
106
+ schools: {
107
+ anyOf: [
108
+ { type: "array", items: { type: "string" } },
109
+ { type: "string" }
110
+ ]
51
111
  },
52
- additionalProperties: false
112
+ keyword: { type: "string" },
113
+ target_count: { type: "integer", minimum: 1 },
114
+ criteria: { type: "string" }
53
115
  },
54
- overrides: {
55
- type: "object",
56
- properties: {
57
- city: { type: "string" },
58
- degree: { type: "string" },
59
- filter_recent_viewed: { type: "boolean" },
60
- schools: {
61
- anyOf: [
62
- { type: "array", items: { type: "string" } },
63
- { type: "string" }
64
- ]
65
- },
66
- keyword: { type: "string" },
67
- target_count: { type: "integer", minimum: 1 }
68
- },
69
- additionalProperties: false
116
+ additionalProperties: false
117
+ }
118
+ },
119
+ required: ["instruction"],
120
+ additionalProperties: false
121
+ };
122
+ }
123
+
124
+ function createToolsSchema() {
125
+ return [
126
+ {
127
+ name: TOOL_RUN_PIPELINE,
128
+ description: "Boss 招聘流水线:默认异步,但会先走与同步一致的前置确认/页面就绪门禁;仅在门禁通过后返回 run_id。传 execution_mode=sync 可改为全程同步执行。",
129
+ inputSchema: createRunInputSchema()
130
+ },
131
+ {
132
+ name: TOOL_START_RUN,
133
+ description: "异步启动 Boss 招聘流水线(含同步门禁预检);只有在前置确认与页面就绪通过后才返回 run_id。",
134
+ inputSchema: createRunInputSchema()
135
+ },
136
+ {
137
+ name: TOOL_GET_RUN,
138
+ description: "按 run_id 查询异步/同步流水线运行状态快照。",
139
+ inputSchema: {
140
+ type: "object",
141
+ properties: {
142
+ run_id: { type: "string" }
143
+ },
144
+ required: ["run_id"],
145
+ additionalProperties: false
146
+ }
147
+ },
148
+ {
149
+ name: TOOL_CANCEL_RUN,
150
+ description: "取消指定 run_id 的运行中流水线。",
151
+ inputSchema: {
152
+ type: "object",
153
+ properties: {
154
+ run_id: { type: "string" }
155
+ },
156
+ required: ["run_id"],
157
+ additionalProperties: false
158
+ }
159
+ }
160
+ ];
161
+ }
162
+
163
+ function createToolResultResponse(id, payload, isError = false) {
164
+ return {
165
+ jsonrpc: "2.0",
166
+ id,
167
+ result: {
168
+ content: [
169
+ {
170
+ type: "text",
171
+ text: JSON.stringify(payload, null, 2)
70
172
  }
173
+ ],
174
+ structuredContent: payload,
175
+ ...(isError ? { isError: true } : {})
176
+ }
177
+ };
178
+ }
179
+
180
+ function validateRunArgs(args) {
181
+ if (!args || typeof args !== "object") {
182
+ return "arguments must be an object";
183
+ }
184
+ if (!args.instruction || typeof args.instruction !== "string") {
185
+ return "instruction is required and must be a string";
186
+ }
187
+ return null;
188
+ }
189
+
190
+ function getLastOutputLine(text) {
191
+ const lines = String(text || "")
192
+ .split(/\r?\n/)
193
+ .map((line) => normalizeText(line))
194
+ .filter(Boolean);
195
+ return lines.length > 0 ? lines[lines.length - 1] : null;
196
+ }
197
+
198
+ function normalizeExecutionMode(value) {
199
+ const normalized = normalizeText(value).toLowerCase();
200
+ if (normalized === RUN_MODE_SYNC) return RUN_MODE_SYNC;
201
+ return RUN_MODE_ASYNC;
202
+ }
203
+
204
+ function buildAsyncPrecheckArgs(args) {
205
+ return {
206
+ instruction: args.instruction,
207
+ confirmation: args.confirmation,
208
+ overrides: args.overrides
209
+ };
210
+ }
211
+
212
+ function safeUpdateRunState(runId, updater) {
213
+ try {
214
+ return updateRunState(runId, updater);
215
+ } catch {
216
+ return null;
217
+ }
218
+ }
219
+
220
+ function safeUpdateRunProgress(runId, patch, message = null) {
221
+ try {
222
+ return updateRunProgress(runId, patch, message);
223
+ } catch {
224
+ return null;
225
+ }
226
+ }
227
+
228
+ function createRuntimeCallbacks(runId, heartbeatIntervalMs) {
229
+ let lastStage = RUN_STAGE_PREFLIGHT;
230
+ let lastOutputPersistAt = 0;
231
+ return {
232
+ heartbeatIntervalMs,
233
+ onStage(event) {
234
+ const stage = normalizeText(event?.stage) || RUN_STAGE_PREFLIGHT;
235
+ lastStage = stage;
236
+ safeUpdateRunState(runId, {
237
+ state: RUN_STATE_RUNNING,
238
+ stage,
239
+ last_message: normalizeText(event?.message || "")
240
+ });
241
+ },
242
+ onHeartbeat(event) {
243
+ const stage = normalizeText(event?.stage) || lastStage;
244
+ lastStage = stage || lastStage;
245
+ const detailsMessage = normalizeText(event?.details?.message || "");
246
+ const patch = { stage: lastStage };
247
+ if (detailsMessage) {
248
+ patch.last_message = detailsMessage;
249
+ }
250
+ safeUpdateRunState(runId, patch);
251
+ try {
252
+ touchRunHeartbeat(runId, detailsMessage || undefined);
253
+ } catch {
254
+ // Ignore heartbeat persistence failures here; state updates above already best-effort.
255
+ }
256
+ },
257
+ onOutput(event) {
258
+ const stage = normalizeText(event?.stage) || lastStage;
259
+ lastStage = stage || lastStage;
260
+ const now = Date.now();
261
+ if (now - lastOutputPersistAt < 1000) return;
262
+ lastOutputPersistAt = now;
263
+ const message = getLastOutputLine(event?.text);
264
+ if (!message) return;
265
+ safeUpdateRunState(runId, {
266
+ stage: lastStage,
267
+ last_message: message
268
+ });
269
+ },
270
+ onProgress(event) {
271
+ const stage = normalizeText(event?.stage) || lastStage;
272
+ lastStage = stage || lastStage;
273
+ safeUpdateRunState(runId, { stage: lastStage });
274
+ safeUpdateRunProgress(
275
+ runId,
276
+ {
277
+ processed: Number.isInteger(event?.processed) ? event.processed : undefined,
278
+ passed: Number.isInteger(event?.passed) ? event.passed : undefined,
279
+ skipped: Number.isInteger(event?.skipped) ? event.skipped : undefined,
280
+ greet_count: Number.isInteger(event?.greet_count) ? event.greet_count : undefined
281
+ },
282
+ normalizeText(event?.line || "")
283
+ );
284
+ },
285
+ getLastStage() {
286
+ return lastStage;
287
+ }
288
+ };
289
+ }
290
+
291
+ function attachSyncRunMetadata(result, runId, lastStage) {
292
+ if (!result || typeof result !== "object") return result;
293
+ if (result.status === "COMPLETED") {
294
+ const nextResult = result.result && typeof result.result === "object" ? result.result : {};
295
+ return {
296
+ ...result,
297
+ result: {
298
+ ...nextResult,
299
+ run_id: runId
300
+ }
301
+ };
302
+ }
303
+ if (result.status === "FAILED") {
304
+ const diagnostics = result.diagnostics && typeof result.diagnostics === "object" ? result.diagnostics : {};
305
+ return {
306
+ ...result,
307
+ diagnostics: {
308
+ ...diagnostics,
309
+ run_id: runId,
310
+ last_stage: lastStage || RUN_STAGE_PREFLIGHT
311
+ }
312
+ };
313
+ }
314
+ return result;
315
+ }
316
+
317
+ async function executeTrackedPipeline({
318
+ runId,
319
+ mode,
320
+ workspaceRoot,
321
+ args,
322
+ signal
323
+ }) {
324
+ const heartbeatIntervalMs = getRunHeartbeatIntervalMs();
325
+ const runtimeCallbacks = createRuntimeCallbacks(runId, heartbeatIntervalMs);
326
+ safeUpdateRunState(runId, {
327
+ state: RUN_STATE_RUNNING,
328
+ stage: RUN_STAGE_PREFLIGHT,
329
+ last_message: "流水线已启动,等待 preflight。"
330
+ });
331
+
332
+ let result;
333
+ try {
334
+ result = await runPipelineImpl(
335
+ {
336
+ workspaceRoot,
337
+ instruction: args.instruction,
338
+ confirmation: args.confirmation,
339
+ overrides: args.overrides
71
340
  },
72
- required: ["instruction"],
73
- additionalProperties: false
341
+ undefined,
342
+ {
343
+ signal,
344
+ heartbeatIntervalMs,
345
+ onStage: runtimeCallbacks.onStage,
346
+ onHeartbeat: runtimeCallbacks.onHeartbeat,
347
+ onOutput: runtimeCallbacks.onOutput,
348
+ onProgress: runtimeCallbacks.onProgress
349
+ }
350
+ );
351
+ } catch (error) {
352
+ const canceled = Boolean(signal?.aborted) || error?.code === "PIPELINE_ABORTED";
353
+ if (canceled) {
354
+ const canceledResult = {
355
+ status: "FAILED",
356
+ error: {
357
+ code: "PIPELINE_CANCELED",
358
+ message: "流水线已取消。",
359
+ retryable: true
360
+ }
361
+ };
362
+ safeUpdateRunState(runId, {
363
+ mode,
364
+ state: RUN_STATE_CANCELED,
365
+ stage: runtimeCallbacks.getLastStage(),
366
+ last_message: "流水线已取消。",
367
+ error: canceledResult.error,
368
+ result: canceledResult
369
+ });
370
+ return {
371
+ result: canceledResult,
372
+ lastStage: runtimeCallbacks.getLastStage(),
373
+ state: RUN_STATE_CANCELED
374
+ };
74
375
  }
376
+
377
+ const failedResult = {
378
+ status: "FAILED",
379
+ error: {
380
+ code: "UNEXPECTED_ERROR",
381
+ message: error?.message || "Unexpected error",
382
+ retryable: true
383
+ }
384
+ };
385
+ safeUpdateRunState(runId, {
386
+ mode,
387
+ state: RUN_STATE_FAILED,
388
+ stage: runtimeCallbacks.getLastStage(),
389
+ last_message: failedResult.error.message,
390
+ error: failedResult.error,
391
+ result: failedResult
392
+ });
393
+ return {
394
+ result: failedResult,
395
+ lastStage: runtimeCallbacks.getLastStage(),
396
+ state: RUN_STATE_FAILED
397
+ };
398
+ }
399
+
400
+ const terminalState = result?.status === "FAILED"
401
+ ? RUN_STATE_FAILED
402
+ : RUN_STATE_COMPLETED;
403
+ safeUpdateRunState(runId, {
404
+ mode,
405
+ state: terminalState,
406
+ stage: runtimeCallbacks.getLastStage(),
407
+ last_message: terminalState === RUN_STATE_COMPLETED ? "流水线执行完成。" : (result?.error?.message || "流水线执行失败。"),
408
+ error: terminalState === RUN_STATE_FAILED ? (result?.error || null) : null,
409
+ result: result || null
410
+ });
411
+ return {
412
+ result,
413
+ lastStage: runtimeCallbacks.getLastStage(),
414
+ state: terminalState
415
+ };
416
+ }
417
+
418
+ function initializeRunStateOrThrow(runId, mode) {
419
+ const snapshot = createRunStateSnapshot({
420
+ runId,
421
+ mode,
422
+ state: "queued",
423
+ stage: RUN_STAGE_PREFLIGHT,
424
+ pid: process.pid,
425
+ lastMessage: "流水线任务已创建,等待执行。"
426
+ });
427
+ return writeRunState(snapshot);
428
+ }
429
+
430
+ async function handleSyncRunTool({ workspaceRoot, args }) {
431
+ cleanupExpiredRuns();
432
+ const runId = createRunId();
433
+ try {
434
+ initializeRunStateOrThrow(runId, RUN_MODE_SYNC);
435
+ } catch (error) {
436
+ return {
437
+ status: "FAILED",
438
+ error: {
439
+ code: "RUN_STATE_IO_ERROR",
440
+ message: `无法写入运行状态目录:${error.message || "unknown"}`,
441
+ retryable: false
442
+ }
443
+ };
444
+ }
445
+ const tracked = await executeTrackedPipeline({
446
+ runId,
447
+ mode: RUN_MODE_SYNC,
448
+ workspaceRoot,
449
+ args,
450
+ signal: null
451
+ });
452
+ return attachSyncRunMetadata(tracked.result, runId, tracked.lastStage);
453
+ }
454
+
455
+ async function handleStartRunTool({ workspaceRoot, args }) {
456
+ const precheckArgs = buildAsyncPrecheckArgs(args);
457
+ let precheckResult;
458
+ try {
459
+ precheckResult = await runPipelineImpl(
460
+ {
461
+ workspaceRoot,
462
+ instruction: precheckArgs.instruction,
463
+ confirmation: precheckArgs.confirmation,
464
+ overrides: precheckArgs.overrides
465
+ },
466
+ undefined,
467
+ {
468
+ precheckOnly: true
469
+ }
470
+ );
471
+ } catch (error) {
472
+ precheckResult = {
473
+ status: "FAILED",
474
+ error: {
475
+ code: "UNEXPECTED_ERROR",
476
+ message: error?.message || "Unexpected error",
477
+ retryable: true
478
+ }
479
+ };
480
+ }
481
+
482
+ if (precheckResult?.status !== PIPELINE_STATUS_READY_TO_START_ASYNC) {
483
+ return precheckResult;
484
+ }
485
+
486
+ cleanupExpiredRuns();
487
+ const runId = createRunId();
488
+ try {
489
+ initializeRunStateOrThrow(runId, RUN_MODE_ASYNC);
490
+ } catch (error) {
491
+ return {
492
+ status: "FAILED",
493
+ error: {
494
+ code: "RUN_STATE_IO_ERROR",
495
+ message: `无法写入运行状态目录:${error.message || "unknown"}`,
496
+ retryable: false
497
+ }
498
+ };
499
+ }
500
+
501
+ const abortController = new AbortController();
502
+ const promise = executeTrackedPipeline({
503
+ runId,
504
+ mode: RUN_MODE_ASYNC,
505
+ workspaceRoot,
506
+ args,
507
+ signal: abortController.signal
508
+ }).finally(() => {
509
+ activeAsyncRuns.delete(runId);
510
+ });
511
+ activeAsyncRuns.set(runId, {
512
+ abortController,
513
+ promise
514
+ });
515
+
516
+ return {
517
+ status: "ACCEPTED",
518
+ run_id: runId,
519
+ state: "queued",
520
+ poll_after_sec: getDefaultPollAfterSec(),
521
+ message: "异步流水线已启动,请使用 get_recruit_pipeline_run 轮询状态。"
522
+ };
523
+ }
524
+
525
+ function handleGetRunTool(args) {
526
+ cleanupExpiredRuns();
527
+ const runId = normalizeText(args?.run_id);
528
+ if (!runId) {
529
+ return {
530
+ status: "FAILED",
531
+ error: {
532
+ code: "INVALID_RUN_ID",
533
+ message: "run_id is required",
534
+ retryable: false
535
+ }
536
+ };
537
+ }
538
+ const snapshot = readRunState(runId);
539
+ if (!snapshot) {
540
+ return {
541
+ status: "FAILED",
542
+ error: {
543
+ code: "RUN_NOT_FOUND",
544
+ message: `未找到 run_id=${runId} 的运行记录。`,
545
+ retryable: false
546
+ }
547
+ };
548
+ }
549
+ return {
550
+ status: "RUN_STATUS",
551
+ run: snapshot
552
+ };
553
+ }
554
+
555
+ function handleCancelRunTool(args) {
556
+ const runId = normalizeText(args?.run_id);
557
+ if (!runId) {
558
+ return {
559
+ status: "FAILED",
560
+ error: {
561
+ code: "INVALID_RUN_ID",
562
+ message: "run_id is required",
563
+ retryable: false
564
+ }
565
+ };
566
+ }
567
+ const snapshot = readRunState(runId);
568
+ if (!snapshot) {
569
+ return {
570
+ status: "FAILED",
571
+ error: {
572
+ code: "RUN_NOT_FOUND",
573
+ message: `未找到 run_id=${runId} 的运行记录。`,
574
+ retryable: false
575
+ }
576
+ };
577
+ }
578
+
579
+ if ([RUN_STATE_COMPLETED, RUN_STATE_FAILED, RUN_STATE_CANCELED].includes(snapshot.state)) {
580
+ return {
581
+ status: "CANCEL_IGNORED",
582
+ run: snapshot,
583
+ message: "目标任务已结束,无需取消。"
584
+ };
585
+ }
586
+
587
+ const activeRun = activeAsyncRuns.get(runId);
588
+ if (activeRun?.abortController) {
589
+ activeRun.abortController.abort();
590
+ }
591
+ safeUpdateRunState(runId, {
592
+ stage: snapshot.stage || RUN_STAGE_PREFLIGHT,
593
+ last_message: "已收到取消请求,正在停止任务。"
594
+ });
595
+
596
+ const latest = readRunState(runId) || snapshot;
597
+ return {
598
+ status: "CANCEL_REQUESTED",
599
+ run: latest
75
600
  };
76
601
  }
77
602
 
@@ -108,64 +633,56 @@ async function handleRequest(message, workspaceRoot) {
108
633
  jsonrpc: "2.0",
109
634
  id,
110
635
  result: {
111
- tools: [createToolSchema()]
636
+ tools: createToolsSchema()
112
637
  }
113
638
  };
114
639
  }
115
640
 
116
641
  if (method === "tools/call") {
117
- if (!params || params.name !== TOOL_NAME) {
118
- return createJsonRpcError(id, -32602, `Unknown tool: ${params?.name || ""}`);
642
+ const toolName = params?.name;
643
+ const args = params?.arguments || {};
644
+
645
+ if ([TOOL_RUN_PIPELINE, TOOL_START_RUN].includes(toolName)) {
646
+ const inputError = validateRunArgs(args);
647
+ if (inputError) {
648
+ return createJsonRpcError(id, -32602, inputError);
649
+ }
119
650
  }
120
651
 
121
- const args = params.arguments || {};
122
- if (!args.instruction || typeof args.instruction !== "string") {
123
- return createJsonRpcError(id, -32602, "instruction is required and must be a string");
652
+ if ([TOOL_GET_RUN, TOOL_CANCEL_RUN].includes(toolName)) {
653
+ if (!args || typeof args.run_id !== "string" || !normalizeText(args.run_id)) {
654
+ return createJsonRpcError(id, -32602, "run_id is required and must be a string");
655
+ }
124
656
  }
125
657
 
126
658
  try {
127
- const result = await runRecruitPipeline({
128
- workspaceRoot,
129
- instruction: args.instruction,
130
- confirmation: args.confirmation,
131
- overrides: args.overrides
132
- });
133
- return {
134
- jsonrpc: "2.0",
135
- id,
136
- result: {
137
- content: [
138
- {
139
- type: "text",
140
- text: JSON.stringify(result, null, 2)
141
- }
142
- ],
143
- structuredContent: result
144
- }
145
- };
659
+ let payload;
660
+ if (toolName === TOOL_RUN_PIPELINE) {
661
+ const executionMode = normalizeExecutionMode(args.execution_mode);
662
+ payload = executionMode === RUN_MODE_SYNC
663
+ ? await handleSyncRunTool({ workspaceRoot, args })
664
+ : await handleStartRunTool({ workspaceRoot, args });
665
+ } else if (toolName === TOOL_START_RUN) {
666
+ payload = await handleStartRunTool({ workspaceRoot, args });
667
+ } else if (toolName === TOOL_GET_RUN) {
668
+ payload = handleGetRunTool(args);
669
+ } else if (toolName === TOOL_CANCEL_RUN) {
670
+ payload = handleCancelRunTool(args);
671
+ } else {
672
+ return createJsonRpcError(id, -32602, `Unknown tool: ${toolName || ""}`);
673
+ }
674
+ const isError = payload?.status === "FAILED";
675
+ return createToolResultResponse(id, payload, isError);
146
676
  } catch (error) {
147
677
  const failed = {
148
678
  status: "FAILED",
149
679
  error: {
150
680
  code: "UNEXPECTED_ERROR",
151
- message: error.message || "Unexpected error",
681
+ message: error?.message || "Unexpected error",
152
682
  retryable: true
153
683
  }
154
684
  };
155
- return {
156
- jsonrpc: "2.0",
157
- id,
158
- result: {
159
- content: [
160
- {
161
- type: "text",
162
- text: JSON.stringify(failed, null, 2)
163
- }
164
- ],
165
- structuredContent: failed,
166
- isError: true
167
- }
168
- };
685
+ return createToolResultResponse(id, failed, true);
169
686
  }
170
687
  }
171
688
 
@@ -181,9 +698,11 @@ async function handleRequest(message, workspaceRoot) {
181
698
 
182
699
  export function startServer() {
183
700
  const envRoot = process.env.BOSS_WORKSPACE_ROOT;
184
- const thisFile = fileURLToPath(import.meta.url);
185
- const mcpRoot = path.resolve(path.dirname(thisFile), "..");
186
- const workspaceRoot = envRoot ? path.resolve(envRoot) : path.resolve(mcpRoot, "..");
701
+ const workspaceRoot = envRoot
702
+ ? path.resolve(envRoot)
703
+ : process.env.INIT_CWD
704
+ ? path.resolve(process.env.INIT_CWD)
705
+ : path.resolve(process.cwd());
187
706
  let buffer = Buffer.alloc(0);
188
707
  let framing = FRAMING_UNKNOWN;
189
708
 
@@ -274,7 +793,16 @@ export function startServer() {
274
793
  });
275
794
  }
276
795
 
796
+ export const __testables = {
797
+ handleRequest,
798
+ activeAsyncRuns,
799
+ setRunPipelineImplForTests(nextImpl) {
800
+ runPipelineImpl = typeof nextImpl === "function" ? nextImpl : runRecruitPipeline;
801
+ }
802
+ };
803
+
277
804
  const thisFilePath = fileURLToPath(import.meta.url);
278
805
  if (process.argv[1] && path.resolve(process.argv[1]) === thisFilePath) {
279
806
  startServer();
280
807
  }
808
+