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 +73 -47
- package/package.json +2 -2
- package/src/offerguard-tools.js +124 -74
- package/src/server.js +18 -2
package/README.md
CHANGED
|
@@ -1,12 +1,49 @@
|
|
|
1
1
|
# OfferGuard MCP Server
|
|
2
2
|
|
|
3
|
-
OfferGuard MCP Server
|
|
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
|
-
- `
|
|
8
|
-
- `
|
|
9
|
-
- `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
env: {}
|
|
65
|
+
http://127.0.0.1:3000/sse
|
|
66
|
+
http://127.0.0.1:3000/messages
|
|
42
67
|
```
|
|
43
68
|
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
+
```bash
|
|
78
|
+
npx -y offerguard-mcp-server@0.3.0
|
|
79
|
+
```
|
|
71
80
|
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "OfferGuard MCP Server for internship offer
|
|
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",
|
package/src/offerguard-tools.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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*\/月)
|
|
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 =>
|
|
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 =
|
|
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.
|
|
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:
|
|
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 = [
|
|
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
|
|
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.
|
|
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
|
|
317
|
-
const studentText = normalizeText(input
|
|
318
|
-
const riskResult = input
|
|
319
|
-
const complianceResult = input
|
|
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.
|
|
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
|
|
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.
|
|
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"
|