offerguard-mcp-server 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,12 +1,49 @@
1
1
  # OfferGuard MCP Server
2
2
 
3
- OfferGuard MCP Server is a real Model Context Protocol service for internship offer risk auditing. It exposes deterministic OfferGuard tools through MCP Streamable HTTP, legacy SSE, and stdio transports.
3
+ OfferGuard MCP Server provides real MCP tools for internship offer risk auditing, compliance checks, seven-dimension scoring, and workflow routing.
4
4
 
5
5
  ## Tools
6
6
 
7
- - `risk_term_matcher`: matches JD risk terms and returns severity, evidence, and trigger reasons.
8
- - `internship_compliance_checker`: checks unpaid trials, deposits, training loans, agreements, work hours, salary payment, IP ownership, and relocation risk.
9
- - `offer_score_calculator`: returns seven-dimension scores, overall score, risk level, and recommendation route.
7
+ - `offerguard_audit`: all-in-one workflow tool for XFYun Agent workflow nodes. Input: `AGENT_USER_INPUT`.
8
+ - `risk_term_matcher`: matches risky JD terms and returns evidence.
9
+ - `internship_compliance_checker`: checks internship red lines such as unpaid trials, deposits, training loans, work hours, salary payment, and IP ownership.
10
+ - `offer_score_calculator`: calculates seven-dimension scores, overall score, risk level, and route.
11
+
12
+ ## Recommended XFYun Workflow Usage
13
+
14
+ For XFYun workflow MCP nodes, prefer the single entry tool:
15
+
16
+ ```json
17
+ {
18
+ "tool": "offerguard_audit",
19
+ "arguments": {
20
+ "AGENT_USER_INPUT": "{{开始.AGENT_USER_INPUT}}"
21
+ }
22
+ }
23
+ ```
24
+
25
+ The tool returns structured JSON with these key fields:
26
+
27
+ - `mcp_ok`
28
+ - `route`
29
+ - `risk_level`
30
+ - `overall_score`
31
+ - `route_marker`
32
+ - `route_real_marker`
33
+ - `risk_terms`
34
+ - `compliance_red_lines`
35
+ - `compliance_missing_items`
36
+ - `dimensions`
37
+ - `required_followup_questions`
38
+ - `negotiation_cards`
39
+ - `accept_conditions`
40
+ - `mcp_audit_summary`
41
+
42
+ If the workflow brancher needs a text marker, read either:
43
+
44
+ - `ROUTE: reject`
45
+ - `ROUTE_REAL: compare`
46
+ - `ROUTE_REAL: negotiate`
10
47
 
11
48
  ## Local Run
12
49
 
@@ -16,61 +53,50 @@ npm test
16
53
  npm start
17
54
  ```
18
55
 
19
- Default local HTTP endpoints:
20
-
21
- - Health: `http://127.0.0.1:3000/health`
22
- - MCP Streamable HTTP: `http://127.0.0.1:3000/mcp`
23
- - Legacy SSE: `http://127.0.0.1:3000/sse`
24
- - Legacy messages: `http://127.0.0.1:3000/messages`
56
+ Streamable HTTP endpoint:
25
57
 
26
- Stdio entry for MCP hosting platforms:
27
-
28
- ```bash
29
- npx -y offerguard-mcp-server@0.2.0
58
+ ```text
59
+ http://127.0.0.1:3000/mcp
30
60
  ```
31
61
 
32
- ## XFYun MCP Hosting
33
-
34
- Use the XFYun MCP hosting platform to host this package as a tool-type MCP server.
35
-
36
- Recommended cloud hosting configuration:
62
+ Legacy SSE endpoints:
37
63
 
38
64
  ```text
39
- command: npx
40
- args: -y offerguard-mcp-server@0.2.0
41
- env: {}
65
+ http://127.0.0.1:3000/sse
66
+ http://127.0.0.1:3000/messages
42
67
  ```
43
68
 
44
- After hosting, the tool list must show:
45
-
46
- - `risk_term_matcher`
47
- - `internship_compliance_checker`
48
- - `offer_score_calculator`
49
-
50
- If the hosted MCP detail page does not show these three tool names, do not connect it to the main OfferGuard workflow.
51
-
52
- ## XFYun Workflow Integration
53
-
54
- Place the hosted MCP tool nodes after the material-completeness gate and before the seven-dimension scorer:
69
+ Health check:
55
70
 
56
71
  ```text
57
- 02_资料完整性门禁
58
- -> MCP: risk_term_matcher
59
- -> MCP: internship_compliance_checker
60
- -> MCP: offer_score_calculator
61
- -> 04_七维结构化评分器
72
+ http://127.0.0.1:3000/health
62
73
  ```
63
74
 
64
- The final report must include `MCP工具审计摘要` with:
65
-
66
- - matched risk terms from `risk_term_matcher.risk_terms`
67
- - compliance red lines from `internship_compliance_checker.red_lines`
68
- - score, risk level, and route from `offer_score_calculator`
75
+ ## NPM Usage
69
76
 
70
- If MCP returns empty data or errors, stop the workflow and show:
77
+ ```bash
78
+ npx -y offerguard-mcp-server@0.3.0
79
+ ```
71
80
 
72
- ```text
73
- MCP 工具连接失败,暂不能生成 OfferGuard 风险报告。请稍后重试或检查 MCP Server 状态。
81
+ For MCP stdio clients:
82
+
83
+ ```json
84
+ {
85
+ "mcpServers": {
86
+ "OfferGuard": {
87
+ "command": "npx",
88
+ "args": ["-y", "offerguard-mcp-server@0.3.0"]
89
+ }
90
+ }
91
+ }
74
92
  ```
75
93
 
76
- Do not continue with a prompt-only fallback.
94
+ ## Test Coverage
95
+
96
+ `npm test` verifies:
97
+
98
+ - Streamable HTTP initialization and `tools/list`.
99
+ - stdio initialization and `tools/list`.
100
+ - Calls to all four tools.
101
+ - Low/medium/high-risk demo cases.
102
+ - `offerguard_audit` compatibility with `AGENT_USER_INPUT`.
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "offerguard-mcp-server",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "type": "module",
6
- "description": "OfferGuard MCP Server for internship offer risk auditing.",
6
+ "description": "OfferGuard MCP Server for internship offer auditing, compliance checks, scoring, and workflow routing.",
7
7
  "scripts": {
8
8
  "start": "node src/server.js",
9
9
  "stdio": "node src/stdio.js",
@@ -11,91 +11,70 @@ const RISK_TERMS = [
11
11
  severity: "critical",
12
12
  category: "合规红线",
13
13
  reason: "试岗或实习期间不支付报酬,学生应优先拒绝或要求书面说明。",
14
- aliases: ["试岗无薪", "无薪实习", "不发薪", "不支付薪资", "无报酬试用"]
14
+ aliases: ["试岗无薪", "无薪实习", "不发薪", "不支付薪资", "unpaid trial", "unpaid internship"]
15
15
  },
16
16
  {
17
17
  term: "押金",
18
18
  severity: "critical",
19
19
  category: "合规红线",
20
20
  reason: "要求学生缴纳押金、保证金或类似费用属于高风险信号。",
21
- aliases: ["保证金", "服装费", "工牌费", "设备押金", "入职收费"]
21
+ aliases: ["保证金", "服装费", "工牌费", "设备押金", "入职收费", "deposit", "security deposit"]
22
22
  },
23
23
  {
24
24
  term: "培训贷",
25
25
  severity: "critical",
26
26
  category: "合规红线",
27
27
  reason: "把入职、培训或实习机会与贷款绑定,存在明显求职风险。",
28
- aliases: ["贷款培训", "分期培训", "培训分期"]
28
+ aliases: ["贷款培训", "分期培训", "培训分期", "training loan"]
29
29
  },
30
30
  {
31
31
  term: "薪资面议",
32
32
  severity: "medium",
33
33
  category: "薪资透明",
34
34
  reason: "薪资结构不透明,面试前需要确认日薪、发放周期和绩效占比。",
35
- aliases: ["待遇面议", "薪酬面议", "工资面议"]
35
+ aliases: ["待遇面议", "薪酬面议", "工资面议", "salary negotiable", "pay negotiable"]
36
36
  },
37
37
  {
38
38
  term: "弹性工作",
39
39
  severity: "medium",
40
40
  category: "工作强度",
41
41
  reason: "弹性工作可能掩盖加班或随时响应要求,需要核实边界。",
42
- aliases: ["弹性上班", "时间灵活"]
42
+ aliases: ["弹性上班", "时间灵活", "flexible work", "flexible schedule"]
43
43
  },
44
44
  {
45
45
  term: "抗压强",
46
46
  severity: "medium",
47
47
  category: "工作强度",
48
48
  reason: "强调抗压通常意味着强节奏或高不确定性任务。",
49
- aliases: ["抗压能力强", "能吃苦", "高压环境"]
50
- },
51
- {
52
- term: "狼性文化",
53
- severity: "high",
54
- category: "组织文化",
55
- reason: "狼性、强结果导向等表述可能对应高压管理和工作边界模糊。",
56
- aliases: ["狼性", "强结果导向", "末位淘汰"]
57
- },
58
- {
59
- term: "快速成长",
60
- severity: "low",
61
- category: "成长承诺",
62
- reason: "快速成长本身不是风险,但若缺少导师和产出机制,容易变成空泛承诺。",
63
- aliases: ["高速成长", "成长空间大"]
49
+ aliases: ["抗压能力强", "能吃苦", "高压环境", "stress resistant", "fast pace"]
64
50
  },
65
51
  {
66
52
  term: "加班",
67
53
  severity: "medium",
68
54
  category: "工作强度",
69
55
  reason: "需要确认加班频率、调休、补贴以及是否影响课程安排。",
70
- aliases: ["晚间在线", "周末响应", "随叫随到", "996", "大小周"]
56
+ aliases: ["晚间在线", "周末响应", "随叫随到", "996", "大小周", "overtime", "on call"]
71
57
  },
72
58
  {
73
59
  term: "转正不明",
74
60
  severity: "medium",
75
61
  category: "转正路径",
76
62
  reason: "转正条件不清晰会影响学生对机会成本的判断。",
77
- aliases: ["表现优秀可转正", "有转正机会", "转正另议"]
63
+ aliases: ["表现优秀可转正", "有转正机会", "转正另议", "possible conversion", "conversion chance"]
78
64
  },
79
65
  {
80
66
  term: "职责泛化",
81
67
  severity: "medium",
82
68
  category: "岗位边界",
83
69
  reason: "职责过泛会降低成长确定性,也容易落入杂活型实习。",
84
- aliases: ["其他临时工作", "完成领导安排", "综合事务", "多任务协同"]
70
+ aliases: ["其他临时工作", "完成领导安排", "综合事务", "多任务协同", "other temporary tasks"]
85
71
  },
86
72
  {
87
73
  term: "作品归属公司",
88
74
  severity: "high",
89
75
  category: "知识产权",
90
76
  reason: "作品、代码或账号归属不清会影响简历展示和后续作品集使用。",
91
- aliases: ["作品版权归公司", "代码归公司", "账号归公司", "不得对外展示"]
92
- },
93
- {
94
- term: "自带设备",
95
- severity: "medium",
96
- category: "资源支持",
97
- reason: "要求自带电脑或设备,需要确认补贴、数据安全和损坏责任。",
98
- aliases: ["自带电脑", "自备电脑", "自备设备"]
77
+ aliases: ["作品版权归公司", "代码归公司", "账号归公司", "不得对外展示", "IP ownership", "portfolio not allowed"]
99
78
  }
100
79
  ];
101
80
 
@@ -104,57 +83,61 @@ const COMPLIANCE_RULES = [
104
83
  id: "unpaid_trial",
105
84
  label: "无薪试岗/无薪实习",
106
85
  severity: "critical",
107
- patterns: ["无薪试岗", "试岗无薪", "无薪实习", "不发薪", "不支付薪资", "无报酬试用"],
86
+ patterns: ["无薪试岗", "试岗无薪", "无薪实习", "不发薪", "不支付薪资", "unpaid trial", "unpaid internship"],
108
87
  action: "建议拒绝,除非对方提供合法、明确、书面的报酬与协议安排。"
109
88
  },
110
89
  {
111
90
  id: "deposit_or_fee",
112
91
  label: "押金/保证金/入职收费",
113
92
  severity: "critical",
114
- patterns: ["押金", "保证金", "服装费", "工牌费", "设备押金", "入职收费"],
93
+ patterns: ["押金", "保证金", "服装费", "工牌费", "设备押金", "入职收费", "deposit", "security deposit"],
115
94
  action: "建议拒绝,不应为获得实习机会提前缴费。"
116
95
  },
117
96
  {
118
97
  id: "training_loan",
119
98
  label: "培训贷/分期培训",
120
99
  severity: "critical",
121
- patterns: ["培训贷", "贷款培训", "分期培训", "培训分期"],
100
+ patterns: ["培训贷", "贷款培训", "分期培训", "培训分期", "training loan"],
122
101
  action: "建议拒绝,并保留沟通记录。"
123
102
  },
124
103
  {
125
104
  id: "work_intensity",
126
105
  label: "工作时长和响应边界不清",
127
106
  severity: "high",
128
- patterns: ["996", "大小周", "随叫随到", "晚间在线", "周末响应", "高强度加班"],
107
+ patterns: ["996", "大小周", "随叫随到", "晚间在线", "周末响应", "高强度加班", "on call", "long overtime"],
129
108
  action: "必须在面试中确认工作时长、调休和补贴规则。"
130
109
  },
131
110
  {
132
111
  id: "ip_ownership",
133
112
  label: "作品/代码/账号归属不清",
134
113
  severity: "high",
135
- patterns: ["作品归属公司", "作品版权归公司", "代码归公司", "账号归公司", "不得对外展示"],
114
+ patterns: ["作品归属公司", "作品版权归公司", "代码归公司", "账号归公司", "不得对外展示", "IP ownership", "portfolio not allowed"],
136
115
  action: "必须要求写入协议,确认可否用于简历、作品集或面试展示。"
137
- },
138
- {
139
- id: "remote_or_relocation",
140
- label: "异地到岗/驻场安排",
141
- severity: "medium",
142
- patterns: ["异地到岗", "外派", "驻场", "跨城到岗"],
143
- action: "必须确认住宿、交通、保险和安全支持。"
144
116
  }
145
117
  ];
146
118
 
147
119
  const ROLE_KEYWORDS = {
148
- media: ["新媒体", "运营", "公众号", "小红书", "短视频", "活动策划"],
149
- backend: ["Java", "后端", "接口", "MySQL", "Redis", "计算机", "开源"],
150
- product: ["产品", "需求", "原型", "用户研究", "数据分析"],
151
- design: ["设计", "Figma", "视觉", "作品集", "交互"]
120
+ media: ["新媒体", "运营", "公众号", "小红书", "短视频", "活动策划", "content", "operation", "xiaohongshu"],
121
+ backend: ["Java", "后端", "接口", "MySQL", "Redis", "计算机", "backend", "API"],
122
+ product: ["产品", "需求", "原型", "用户研究", "数据分析", "product", "PRD", "prototype"],
123
+ design: ["设计", "Figma", "视觉", "作品集", "交互", "design"]
152
124
  };
153
125
 
154
126
  function normalizeText(value) {
155
127
  if (!value) return "";
156
128
  if (typeof value === "string") return value;
157
- return JSON.stringify(value);
129
+ return JSON.stringify(value, null, 2);
130
+ }
131
+
132
+ function getPrimaryText(input) {
133
+ return normalizeText(
134
+ input?.AGENT_USER_INPUT ??
135
+ input?.raw_input ??
136
+ input?.rawInput ??
137
+ input?.jdText ??
138
+ input?.text ??
139
+ input
140
+ );
158
141
  }
159
142
 
160
143
  function clamp(score) {
@@ -162,11 +145,14 @@ function clamp(score) {
162
145
  }
163
146
 
164
147
  function includesAny(text, patterns) {
165
- return patterns.some(pattern => text.includes(pattern));
148
+ const lower = text.toLowerCase();
149
+ return patterns.some(pattern => lower.includes(String(pattern).toLowerCase()));
166
150
  }
167
151
 
168
152
  function extractEvidence(text, pattern) {
169
- const index = text.indexOf(pattern);
153
+ const lower = text.toLowerCase();
154
+ const needle = String(pattern).toLowerCase();
155
+ const index = lower.indexOf(needle);
170
156
  if (index < 0) return "";
171
157
  const start = Math.max(0, index - 24);
172
158
  const end = Math.min(text.length, index + pattern.length + 36);
@@ -175,7 +161,7 @@ function extractEvidence(text, pattern) {
175
161
 
176
162
  function findFirstAlias(text, term) {
177
163
  const patterns = [term.term, ...term.aliases];
178
- return patterns.find(pattern => text.includes(pattern));
164
+ return patterns.find(pattern => includesAny(text, [pattern]));
179
165
  }
180
166
 
181
167
  function countBySeverity(items) {
@@ -189,6 +175,7 @@ function countBySeverity(items) {
189
175
  }
190
176
 
191
177
  function highestSeverity(items) {
178
+ if (!items.length) return "none";
192
179
  return items.reduce((highest, item) => {
193
180
  return SEVERITY_WEIGHT[item.severity] > SEVERITY_WEIGHT[highest]
194
181
  ? item.severity
@@ -197,11 +184,11 @@ function highestSeverity(items) {
197
184
  }
198
185
 
199
186
  function salaryIsExplicit(text) {
200
- return /(\d{2,4}\s*[-~至到]\s*\d{2,4}\s*元?\/?(天|日|月)?)|(\d{2,4}\s*元\/?(天|日))|(\d{3,5}\s*\/月)/.test(text);
187
+ return /(\d{2,4}\s*[-~至到]\s*\d{2,4}\s*元?\/?(天|日|月)?)|(\d{2,4}\s*元\/?(天|日))|(\d{3,5}\s*\/月)|(\d{2,4}\s*rmb)/i.test(text);
201
188
  }
202
189
 
203
190
  function hasAgreement(text) {
204
- return includesAny(text, ["实习协议", "三方协议", "劳动合同", "书面协议", "电子签"]);
191
+ return includesAny(text, ["实习协议", "三方协议", "劳动合同", "书面协议", "电子签", "written agreement", "contract"]);
205
192
  }
206
193
 
207
194
  function hasMentor(text) {
@@ -209,18 +196,21 @@ function hasMentor(text) {
209
196
  }
210
197
 
211
198
  function hasConversionPath(text) {
212
- return includesAny(text, ["转正标准", "转正流程", "考核指标", "留用", "转正名额"]);
199
+ return includesAny(text, ["转正标准", "转正流程", "考核指标", "留用", "转正名额", "conversion criteria"]);
213
200
  }
214
201
 
215
202
  function roleMatchBonus(jdText, studentText) {
216
203
  const keywordGroups = Object.values(ROLE_KEYWORDS);
217
- return keywordGroups.some(keywords => keywords.some(word => jdText.includes(word)) && keywords.some(word => studentText.includes(word)))
204
+ return keywordGroups.some(keywords =>
205
+ keywords.some(word => includesAny(jdText, [word])) &&
206
+ keywords.some(word => includesAny(studentText, [word]))
207
+ )
218
208
  ? 10
219
209
  : 0;
220
210
  }
221
211
 
222
212
  export function matchRiskTerms(input) {
223
- const jdText = normalizeText(input.jdText ?? input.text ?? input);
213
+ const jdText = getPrimaryText(input);
224
214
  const matches = RISK_TERMS.map(term => {
225
215
  const matched = findFirstAlias(jdText, term);
226
216
  if (!matched) return null;
@@ -239,25 +229,29 @@ export function matchRiskTerms(input) {
239
229
 
240
230
  return {
241
231
  tool: "risk_term_matcher",
242
- version: "0.2.0",
232
+ version: "0.3.0",
243
233
  risk_terms: matches,
244
234
  summary: {
245
235
  total_hits: matches.length,
246
236
  severity_counts,
247
- highest_severity: matches.length ? highestSeverity(matches) : "none",
237
+ highest_severity: highestSeverity(matches),
248
238
  risk_score_penalty: Math.min(45, penalty)
249
239
  }
250
240
  };
251
241
  }
252
242
 
253
243
  export function checkInternshipCompliance(input) {
254
- const text = [input.jdText, input.offerText, input.extraText]
244
+ const text = [
245
+ getPrimaryText(input),
246
+ input?.offerText,
247
+ input?.extraText
248
+ ]
255
249
  .map(normalizeText)
256
250
  .filter(Boolean)
257
251
  .join("\n");
258
252
 
259
253
  const flags = COMPLIANCE_RULES.map(rule => {
260
- const matched = rule.patterns.find(pattern => text.includes(pattern));
254
+ const matched = rule.patterns.find(pattern => includesAny(text, [pattern]));
261
255
  if (!matched) return null;
262
256
  return {
263
257
  id: rule.id,
@@ -270,7 +264,7 @@ export function checkInternshipCompliance(input) {
270
264
  }).filter(Boolean);
271
265
 
272
266
  const missing_items = [];
273
- if (!salaryIsExplicit(text) || includesAny(text, ["薪资面议", "待遇面议", "工资面议"])) {
267
+ if (!salaryIsExplicit(text) || includesAny(text, ["薪资面议", "待遇面议", "工资面议", "salary negotiable", "pay negotiable"])) {
274
268
  missing_items.push({
275
269
  id: "salary_structure",
276
270
  label: "薪资结构/发放周期",
@@ -286,7 +280,7 @@ export function checkInternshipCompliance(input) {
286
280
  recommended_question: "入职前是否签署书面实习协议,协议中是否包含薪资、工时、保险和作品归属?"
287
281
  });
288
282
  }
289
- if (!hasConversionPath(text) && includesAny(text, ["转正", "留用"])) {
283
+ if (!hasConversionPath(text) && includesAny(text, ["转正", "留用", "conversion"])) {
290
284
  missing_items.push({
291
285
  id: "conversion_criteria",
292
286
  label: "转正标准",
@@ -299,7 +293,7 @@ export function checkInternshipCompliance(input) {
299
293
 
300
294
  return {
301
295
  tool: "internship_compliance_checker",
302
- version: "0.2.0",
296
+ version: "0.3.0",
303
297
  compliance_flags: flags,
304
298
  red_lines,
305
299
  missing_items,
@@ -313,10 +307,10 @@ export function checkInternshipCompliance(input) {
313
307
  }
314
308
 
315
309
  export function calculateOfferScore(input) {
316
- const jdText = normalizeText(input.jdText ?? input.jobProfile);
317
- const studentText = normalizeText(input.studentProfile);
318
- const riskResult = input.riskTermResult?.risk_terms ? input.riskTermResult : matchRiskTerms({ jdText });
319
- const complianceResult = input.complianceResult?.summary ? input.complianceResult : checkInternshipCompliance({ jdText });
310
+ const jdText = normalizeText(input?.jdText ?? input?.jobProfile ?? getPrimaryText(input));
311
+ const studentText = normalizeText(input?.studentProfile ?? input?.AGENT_USER_INPUT ?? input?.raw_input ?? "");
312
+ const riskResult = input?.riskTermResult?.risk_terms ? input.riskTermResult : matchRiskTerms({ jdText });
313
+ const complianceResult = input?.complianceResult?.summary ? input.complianceResult : checkInternshipCompliance({ jdText });
320
314
 
321
315
  const riskTerms = riskResult.risk_terms ?? [];
322
316
  const flags = complianceResult.compliance_flags ?? [];
@@ -331,10 +325,10 @@ export function calculateOfferScore(input) {
331
325
  const explicitSalary = salaryIsExplicit(jdText);
332
326
  const mentor = hasMentor(jdText);
333
327
  const conversionPath = hasConversionPath(jdText);
334
- const projectValue = includesAny(jdText, ["上线", "核心项目", "真实项目", "可量化", "作品", "数据复盘"]);
335
- const repetitive = includesAny(jdText, ["资料整理", "简单重复", "打杂", "行政杂务", "搬运"]);
336
- const overtime = includesAny(jdText, ["996", "大小周", "随叫随到", "晚间在线", "周末响应", "加班"]);
337
- const wantsStableHours = includesAny(studentText, ["不希望长期夜间", "不接受加班", "课业", "考研", "保研"]);
328
+ const projectValue = includesAny(jdText, ["上线", "核心项目", "真实项目", "可量化", "作品", "数据复盘", "portfolio", "project"]);
329
+ const repetitive = includesAny(jdText, ["资料整理", "简单重复", "打杂", "行政杂务", "搬运", "repetitive"]);
330
+ const overtime = includesAny(jdText, ["996", "大小周", "随叫随到", "晚间在线", "周末响应", "加班", "overtime", "on call"]);
331
+ const wantsStableHours = includesAny(studentText, ["不希望长期夜间", "不接受加班", "不希望长期高强度", "课程", "考试", "stable hours"]);
338
332
 
339
333
  const dimensions = [
340
334
  {
@@ -352,7 +346,7 @@ export function calculateOfferScore(input) {
352
346
  {
353
347
  key: "salary_transparency",
354
348
  name: "薪资透明",
355
- score: clamp(58 + (explicitSalary ? 20 : -12) - (includesAny(jdText, ["薪资面议", "待遇面议"]) ? 15 : 0)),
349
+ score: clamp(58 + (explicitSalary ? 20 : -12) - (includesAny(jdText, ["薪资面议", "待遇面议", "salary negotiable"]) ? 15 : 0)),
356
350
  rationale: "薪资金额、发放周期和绩效占比越明确,透明度越高。"
357
351
  },
358
352
  {
@@ -370,7 +364,7 @@ export function calculateOfferScore(input) {
370
364
  {
371
365
  key: "conversion_clarity",
372
366
  name: "转正清晰",
373
- score: clamp(62 + (conversionPath ? 20 : 0) - (includesAny(jdText, ["转正另议", "表现优秀可转正", "有转正机会"]) ? 15 : 0)),
367
+ score: clamp(62 + (conversionPath ? 20 : 0) - (includesAny(jdText, ["转正另议", "表现优秀可转正", "有转正机会", "possible conversion"]) ? 15 : 0)),
374
368
  rationale: "有量化转正标准和流程时分数更高。"
375
369
  },
376
370
  {
@@ -406,10 +400,12 @@ export function calculateOfferScore(input) {
406
400
 
407
401
  return {
408
402
  tool: "offer_score_calculator",
409
- version: "0.2.0",
403
+ version: "0.3.0",
410
404
  overall_score,
411
405
  risk_level,
412
406
  route,
407
+ route_marker: `ROUTE: ${route}`,
408
+ route_real_marker: `ROUTE_REAL: ${route}`,
413
409
  one_sentence_reason,
414
410
  dimensions,
415
411
  decision_rules_triggered: {
@@ -424,3 +420,57 @@ export function calculateOfferScore(input) {
424
420
  ].slice(0, 6)
425
421
  };
426
422
  }
423
+
424
+ export function auditOfferGuardInput(input) {
425
+ const raw_input = getPrimaryText(input);
426
+ const risk = matchRiskTerms({ jdText: raw_input });
427
+ const compliance = checkInternshipCompliance({ jdText: raw_input, extraText: raw_input });
428
+ const score = calculateOfferScore({
429
+ jdText: raw_input,
430
+ studentProfile: raw_input,
431
+ riskTermResult: risk,
432
+ complianceResult: compliance
433
+ });
434
+
435
+ const mustAsk = [
436
+ ...score.required_followup_questions,
437
+ "岗位的核心工作占比、导师机制和前三周交付物分别是什么?",
438
+ "Offer 或实习协议中能否书面确认薪资、工时、到岗、作品归属和离职规则?"
439
+ ].slice(0, 6);
440
+
441
+ return {
442
+ tool: "offerguard_audit",
443
+ version: "0.3.0",
444
+ mcp_ok: true,
445
+ raw_input_length: raw_input.length,
446
+ route: score.route,
447
+ risk_level: score.risk_level,
448
+ overall_score: score.overall_score,
449
+ route_marker: score.route_marker,
450
+ route_real_marker: score.route_real_marker,
451
+ one_sentence_reason: score.one_sentence_reason,
452
+ risk_terms: risk.risk_terms,
453
+ compliance_red_lines: compliance.red_lines,
454
+ compliance_missing_items: compliance.missing_items,
455
+ dimensions: score.dimensions,
456
+ required_followup_questions: mustAsk,
457
+ negotiation_cards: [
458
+ "您好,我想确认一下这个岗位的薪资结构、发放周期和是否有绩效/扣款规则,方便我判断是否匹配实习安排。",
459
+ "我希望提前了解实习协议中对工作时间、加班调休、作品归属和实习证明的约定,是否可以在入职前书面确认?",
460
+ "如果后续有转正机会,能否请您说明考核指标、时间节点和审批流程?"
461
+ ],
462
+ accept_conditions: [
463
+ "薪资、发放周期、到岗时间和工时边界可以书面确认。",
464
+ "没有无薪试岗、押金、培训贷等合规红线。",
465
+ "导师机制、核心任务和可沉淀产出明确。",
466
+ "转正或实习证明规则清晰,不以口头承诺替代书面约定。"
467
+ ],
468
+ mcp_audit_summary: {
469
+ risk_hit_count: risk.summary.total_hits,
470
+ red_line_count: compliance.summary.red_line_count,
471
+ missing_item_count: compliance.summary.missing_item_count,
472
+ score_route: score.route,
473
+ score_risk_level: score.risk_level
474
+ }
475
+ };
476
+ }
package/src/server.js CHANGED
@@ -5,15 +5,16 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
5
5
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
6
6
  import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
7
7
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
8
- import * as z from "zod/v4";
8
+ import { z } from "zod";
9
9
  import {
10
+ auditOfferGuardInput,
10
11
  calculateOfferScore,
11
12
  checkInternshipCompliance,
12
13
  matchRiskTerms
13
14
  } from "./offerguard-tools.js";
14
15
 
15
16
  const SERVICE_NAME = "OfferGuardMCP";
16
- const SERVICE_VERSION = "0.2.0";
17
+ const SERVICE_VERSION = "0.3.0";
17
18
 
18
19
  function jsonToolResult(result) {
19
20
  return {
@@ -33,6 +34,20 @@ export function createOfferGuardMcpServer() {
33
34
  version: SERVICE_VERSION
34
35
  });
35
36
 
37
+ server.registerTool(
38
+ "offerguard_audit",
39
+ {
40
+ description: "All-in-one OfferGuard audit for XFYun workflow MCP nodes. It accepts AGENT_USER_INPUT and returns route, scores, risk terms, compliance flags, negotiation cards, and MCP audit summary.",
41
+ inputSchema: {
42
+ AGENT_USER_INPUT: z
43
+ .string()
44
+ .min(1)
45
+ .describe("Raw user input from the Agent workflow, including JD, resume/profile, preferences, and risk tolerance.")
46
+ }
47
+ },
48
+ async input => jsonToolResult(auditOfferGuardInput(input))
49
+ );
50
+
36
51
  server.registerTool(
37
52
  "risk_term_matcher",
38
53
  {
@@ -86,6 +101,7 @@ export function createApp({ host = process.env.HOST ?? "127.0.0.1" } = {}) {
86
101
  status: "ok",
87
102
  transports: ["streamable-http", "sse-legacy"],
88
103
  tools: [
104
+ "offerguard_audit",
89
105
  "risk_term_matcher",
90
106
  "internship_compliance_checker",
91
107
  "offer_score_calculator"