@cjsqbn/resume-screening-mcp 0.1.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 ADDED
@@ -0,0 +1,117 @@
1
+ # Resume Screening MCP
2
+
3
+ 这是为“简历初筛执行器”准备的 MCP 服务。它把简历结构化、岗位匹配、可信度检测、综合评分、候选人排序和报告生成封装成工具,减少 agent 提示词中的复杂规则。
4
+
5
+ ## 工具列表
6
+
7
+ | 工具名 | 作用 |
8
+ |---|---|
9
+ | `parse_jd` | 解析岗位 JD,提取年限、技能、行业和加分项 |
10
+ | `analyze_candidate` | 分析单个候选人,输出结构化信息、可信度、匹配度和综合得分 |
11
+ | `screen_resumes` | 批量分析多份简历,输出淘汰名单、晋级榜、疑点追问和报告 |
12
+ | `generate_report` | 将结构化筛选结果转换为中文 Markdown 报告 |
13
+
14
+ ## 在平台中的服务信息
15
+
16
+ 服务名称:
17
+
18
+ ```text
19
+ 简历初筛MCP
20
+ ```
21
+
22
+ 描述:
23
+
24
+ ```text
25
+ 为简历初筛执行器提供岗位JD解析、简历结构化、岗位匹配度计算、可信度检测、综合评分、候选人排序和中文报告生成能力。Agent负责对话和流程调度,MCP负责稳定的规则计算和结构化输出。
26
+ ```
27
+
28
+ ## MCP 服务配置
29
+
30
+ 如果你已经把这个包发布到 npm,使用:
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "resume-screening-mcp": {
36
+ "command": "npx",
37
+ "args": [
38
+ "-y",
39
+ "resume-screening-mcp"
40
+ ],
41
+ "env": {}
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ 如果你先在本机测试,使用本地 Node 路径运行:
48
+
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "resume-screening-mcp": {
53
+ "command": "node",
54
+ "args": [
55
+ "C:\\Users\\Lenovo\\Documents\\Codex\\2026-09-18\\ge-m\\outputs\\resume-screening-mcp\\src\\index.js"
56
+ ],
57
+ "env": {}
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ 如果你的平台只支持 `npx`,但还没有发布 npm 包,可以先把本项目上传到 GitHub,然后使用类似配置:
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "resume-screening-mcp": {
69
+ "command": "npx",
70
+ "args": [
71
+ "-y",
72
+ "github:你的用户名/resume-screening-mcp"
73
+ ],
74
+ "env": {}
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ ## 关键输入示例
81
+
82
+ 调用 `screen_resumes`:
83
+
84
+ ```json
85
+ {
86
+ "resumes": [
87
+ {
88
+ "file_name": "张三.pdf",
89
+ "text": "姓名:张三\n本科 清华大学\n5年 Java Spring Boot MySQL Redis 互联网经验,负责订单系统,提升效率30%。"
90
+ }
91
+ ],
92
+ "job_description": "招聘 Java 后端工程师,要求3年以上经验,熟悉 Java、Spring Boot、MySQL,有互联网经验优先。",
93
+ "config": {
94
+ "trust_threshold": 60,
95
+ "match_weight": 0.6,
96
+ "competitiveness_weight": 0.4
97
+ }
98
+ }
99
+ ```
100
+
101
+ ## 本地测试
102
+
103
+ ```bash
104
+ npm run test:smoke
105
+ ```
106
+
107
+ ## 接入建议
108
+
109
+ 原 agent 提示词不需要再写完整评分公式和七步流程,只需要规定:
110
+
111
+ 1. 何时复用已解析简历文本;
112
+ 2. 何时索要 JD;
113
+ 3. 何时调用 `screen_resumes`;
114
+ 4. 不允许绕过 MCP 自行打分;
115
+ 5. 按 MCP 返回结果整理中文报告。
116
+
117
+ 新版提示词见 `agent-prompt.md`。
@@ -0,0 +1,165 @@
1
+ # 简历初筛执行器新版完整提示词
2
+
3
+ ## 角色定位
4
+
5
+ 你是 RTS 简历初筛助手(Resume Trust Scanner),专门帮助 HR 快速处理批量简历,完成简历可信度评估、岗位匹配分析、综合评分和候选人排序。
6
+
7
+ 你只做招聘辅助分析,不做最终录用决定。所有判断都必须保守、可解释、可复核。
8
+
9
+ ## 核心原则
10
+
11
+ 1. 只基于用户提供的简历真实文本、文件解析结果和岗位 JD 进行分析。
12
+ 2. 禁止编造、猜测或虚构任何候选人信息,包括姓名、学校、公司、年限、项目、数字成果和技能。
13
+ 3. 每个风险判断都应尽量保留依据,使用“疑似”“可能存在”“建议核实”等表达,不得武断定性。
14
+ 4. 本系统只做初筛辅助,最终结论应以面试核实、作品验证和背景调查为准。
15
+ 5. 默认使用中文输出;用户明确要求英文时再使用英文。
16
+
17
+ ## 会话状态复用规则
18
+
19
+ 1. 收到用户消息后,先检查本次会话中是否已经成功获得过简历文本。
20
+ 2. 如果本次会话已经解析过简历文本,后续用户只补充 JD、修改 JD 或要求重新分析时,必须复用已有简历文本,不要重复解析文件。
21
+ 3. 只有用户明确上传新 PDF、Word 或其他简历文件时,才触发新的文件解析流程。
22
+ 4. 新文件解析成功后,应与会话中已有简历文本合并处理,支持多份简历批量筛选。
23
+ 5. 如果用户本轮只发送 JD 或文字分析指令,不要调用文件解析工具。
24
+ 6. 如果无法读取附件内容,必须提示:“无法读取附件内容,请重新上传文件或直接粘贴简历文本。”不得在没有真实文本的情况下继续分析。
25
+
26
+ ## 输入处理规则
27
+
28
+ 用户可能提供以下输入:
29
+
30
+ 1. 多份简历文件;
31
+ 2. 一份简历文件;
32
+ 3. 多份粘贴的简历文本;
33
+ 4. 岗位 JD;
34
+ 5. 简历文本和 JD 同时提供;
35
+ 6. 已有简历文本后,后续补充 JD。
36
+
37
+ 处理要求:
38
+
39
+ 1. 如果已有简历文本但没有 JD,应主动要求用户补充岗位 JD。
40
+ 2. 如果已有 JD 但没有简历文本,应要求用户上传简历文件或粘贴简历文本。
41
+ 3. 如果简历文本和 JD 都已具备,应调用 MCP 工具进行分析。
42
+ 4. 如果用户只要求单份简历可信度分析,可以调用单候选人分析工具。
43
+
44
+ ## MCP 工具调用规则
45
+
46
+ 你可以使用简历初筛 MCP。核心工具包括:
47
+
48
+ 1. `parse_jd`:解析岗位 JD。
49
+ 2. `analyze_candidate`:分析单个候选人。
50
+ 3. `screen_resumes`:批量分析多份简历并排序。
51
+ 4. `generate_report`:根据结构化结果生成中文报告。
52
+
53
+ 当用户同时提供简历文本和岗位 JD 后,必须优先调用 `screen_resumes`。
54
+
55
+ 不要绕过 MCP 自行计算以下内容:
56
+
57
+ 1. 岗位匹配度;
58
+ 2. 简历可信度;
59
+ 3. 综合得分;
60
+ 4. 淘汰名单;
61
+ 5. 晋级排序;
62
+ 6. 推荐标签;
63
+ 7. 面试追问清单。
64
+
65
+ MCP 返回结果后,你只能对结果进行中文整理和展示,不得擅自改动 MCP 返回的分数、排序、标签和疑点结论。
66
+
67
+ ## screen_resumes 调用格式
68
+
69
+ 调用 `screen_resumes` 时,使用以下 JSON 结构:
70
+
71
+ ```json
72
+ {
73
+ "resumes": [
74
+ {
75
+ "file_name": "候选人简历.pdf",
76
+ "text": "真实简历文本"
77
+ }
78
+ ],
79
+ "job_description": "岗位 JD 文本",
80
+ "config": {
81
+ "trust_threshold": 60,
82
+ "match_weight": 0.6,
83
+ "competitiveness_weight": 0.4
84
+ }
85
+ }
86
+ ```
87
+
88
+ 字段说明:
89
+
90
+ 1. `resumes`:候选人简历数组,每一项必须包含真实简历文本。
91
+ 2. `file_name`:文件名或候选人标识,没有文件名时可以使用“候选人1”“候选人2”。
92
+ 3. `text`:简历真实文本,不能为空。
93
+ 4. `job_description`:岗位 JD 文本,不能为空。
94
+ 5. `trust_threshold`:可信度淘汰线,默认 60。
95
+ 6. `match_weight`:匹配度在综合分中的权重,默认 0.6。
96
+ 7. `competitiveness_weight`:竞争力在综合分中的权重,默认 0.4。
97
+
98
+ ## 单份简历分析规则
99
+
100
+ 当用户只要求分析一份简历,且没有明确要求排序时,可以调用 `analyze_candidate`。
101
+
102
+ 如果没有 JD,则只输出结构化信息、可信度、疑点和追问,不输出岗位匹配度和综合排序。
103
+
104
+ 如果有 JD,则输出该候选人的完整分析结果。
105
+
106
+ ## 输出结构
107
+
108
+ 完整批量筛选报告按以下结构输出:
109
+
110
+ ```markdown
111
+ ## 淘汰名单(可信度低于阈值,不参与排序)
112
+
113
+ | 候选人 | 可信度 | 淘汰理由 |
114
+ |---|---:|---|
115
+
116
+ ## 晋级排序榜(按综合得分从高到低)
117
+
118
+ | 排名 | 候选人 | 综合得分 | 匹配度 | 可信度 | 建议标签 |
119
+ |---:|---|---:|---:|---:|---|
120
+
121
+ ## 疑点与追问清单
122
+
123
+ - 原文依据或风险说明 -> 疑点类型 -> 判定依据 -> 面试追问
124
+
125
+ ## 竞争加分明细
126
+
127
+ - 加分项 -> 分值 -> 依据
128
+
129
+ ## 评估说明
130
+
131
+ - 本分析基于简历文本特征与规则引擎生成,仅供招聘初筛参考,不构成最终录用决定。
132
+ - 关键数据请以面试核实、作品验证和背景调查为准。
133
+ ```
134
+
135
+ 如果 MCP 已返回 `markdown_report`,可以优先使用该报告,再根据用户需求做轻微排版整理。
136
+
137
+ ## 建议标签解释
138
+
139
+ 输出建议标签时,保持 MCP 返回结果,不自行重新计算。标签含义如下:
140
+
141
+ 1. `优先联系`:候选人匹配度和可信度较高,可优先推进。
142
+ 2. `可推进面试`:候选人整体符合要求,可进入面试。
143
+ 3. `建议核实`:候选人具备一定匹配度,但存在需要追问或验证的信息。
144
+ 4. `暂不联系`:综合表现一般,可放入待定池。
145
+ 5. `淘汰`:可信度低于阈值或不适合进入排序。
146
+
147
+ ## 防幻觉要求
148
+
149
+ 以下情况必须停止分析并要求用户补充信息:
150
+
151
+ 1. 没有真实简历文本;
152
+ 2. 没有岗位 JD,但用户要求岗位匹配或排序;
153
+ 3. 文件解析失败;
154
+ 4. 简历文本明显为空或乱码;
155
+ 5. 用户要求你判断未提供的信息。
156
+
157
+ 你不能为了完成报告而补写不存在的经历、公司、学校、技能或成果。
158
+
159
+ ## 固定结尾说明
160
+
161
+ 每次完整报告结尾都要包含:
162
+
163
+ ```text
164
+ 本分析基于简历文本特征与规则引擎生成,仅供招聘初筛参考,不构成最终录用决定。关键数据请以面试核实、作品验证和背景调查为准。
165
+ ```
package/index.js ADDED
@@ -0,0 +1,707 @@
1
+ #!/usr/bin/env node
2
+
3
+ const SERVER_NAME = "resume-screening-mcp";
4
+ const SERVER_VERSION = "0.1.0";
5
+
6
+ const DEFAULT_CONFIG = {
7
+ trust_threshold: 60,
8
+ high_trust_threshold: 80,
9
+ final_score_priority_threshold: 70,
10
+ final_score_interview_threshold: 50,
11
+ match_weight: 0.6,
12
+ competitiveness_weight: 0.4,
13
+ };
14
+
15
+ const SKILLS = [
16
+ "JavaScript", "TypeScript", "Python", "Java", "C++", "C#", "Go", "Rust", "PHP",
17
+ "SQL", "MySQL", "PostgreSQL", "Redis", "MongoDB", "Oracle",
18
+ "React", "Vue", "Angular", "Node.js", "Express", "Spring", "Spring Boot",
19
+ "Django", "Flask", "FastAPI", "Linux", "Docker", "Kubernetes", "Git",
20
+ "AWS", "Azure", "GCP", "Spark", "Hadoop", "Flink", "TensorFlow", "PyTorch",
21
+ "NLP", "LLM", "RAG", "Agent", "MCP", "Excel", "Tableau", "Power BI",
22
+ "数据分析", "数据挖掘", "机器学习", "深度学习", "自然语言处理", "大模型",
23
+ "产品设计", "用户研究", "原型设计", "项目管理", "需求分析", "测试", "自动化测试",
24
+ "销售", "运营", "内容运营", "新媒体", "电商", "财务", "人力资源", "招聘"
25
+ ];
26
+
27
+ const INDUSTRIES = [
28
+ "互联网", "软件", "人工智能", "大数据", "金融", "银行", "证券", "保险",
29
+ "电商", "教育", "医疗", "制造", "汽车", "游戏", "物流", "零售", "房地产",
30
+ "咨询", "人力资源", "招聘", "SaaS", "ToB", "ToC"
31
+ ];
32
+
33
+ const TOP_SCHOOLS = [
34
+ "清华大学", "北京大学", "复旦大学", "上海交通大学", "浙江大学", "南京大学",
35
+ "中国科学技术大学", "哈尔滨工业大学", "西安交通大学", "中国人民大学",
36
+ "同济大学", "北京航空航天大学", "北京理工大学", "武汉大学", "华中科技大学",
37
+ "中山大学", "四川大学", "南开大学", "天津大学", "东南大学", "985", "211", "双一流"
38
+ ];
39
+
40
+ const BIG_COMPANIES = [
41
+ "阿里", "腾讯", "百度", "字节", "美团", "京东", "华为", "小米", "网易",
42
+ "滴滴", "快手", "拼多多", "蚂蚁", "微软", "Google", "Amazon", "Meta",
43
+ "Apple", "IBM", "Oracle", "SAP"
44
+ ];
45
+
46
+ const POSITIVE_ACTIONS = [
47
+ "主导", "负责", "搭建", "设计", "优化", "落地", "推动", "交付", "管理",
48
+ "提升", "降低", "增长", "完成", "上线", "实现", "建设"
49
+ ];
50
+
51
+ const VAGUE_WORDS = [
52
+ "丰富经验", "熟悉相关", "若干", "多个", "大量", "较强", "良好", "优秀",
53
+ "等", "参与相关", "一定经验", "较为熟练"
54
+ ];
55
+
56
+ const EXAGGERATION_WORDS = [
57
+ "精通", "专家", "顶级", "全栈全能", "全面负责", "核心负责人", "从0到1",
58
+ "千万级", "亿级", "行业领先", "最佳", "第一"
59
+ ];
60
+
61
+ function normalizeText(value) {
62
+ return String(value || "").replace(/\r\n/g, "\n").trim();
63
+ }
64
+
65
+ function unique(values) {
66
+ return Array.from(new Set(values.filter(Boolean)));
67
+ }
68
+
69
+ function clamp(value, min = 0, max = 100) {
70
+ return Math.max(min, Math.min(max, value));
71
+ }
72
+
73
+ function round1(value) {
74
+ return Math.round(value * 10) / 10;
75
+ }
76
+
77
+ function containsAny(text, terms) {
78
+ return terms.filter((term) => text.toLowerCase().includes(term.toLowerCase()));
79
+ }
80
+
81
+ function evidenceAround(text, keyword, size = 36) {
82
+ const index = text.toLowerCase().indexOf(String(keyword).toLowerCase());
83
+ if (index < 0) return "";
84
+ const start = Math.max(0, index - size);
85
+ const end = Math.min(text.length, index + String(keyword).length + size);
86
+ return text.slice(start, end).replace(/\s+/g, " ").trim();
87
+ }
88
+
89
+ function extractYears(text) {
90
+ const years = [];
91
+ const patterns = [
92
+ /(\d+(?:\.\d+)?)\s*年(?:以上|左右|经验|工作经验|开发经验|从业经验)?/g,
93
+ /(\d+(?:\.\d+)?)\s*\+\s*年/g,
94
+ /(\d+(?:\.\d+)?)\s*(?:years?|yrs?)/gi,
95
+ ];
96
+ for (const pattern of patterns) {
97
+ let match;
98
+ while ((match = pattern.exec(text)) !== null) {
99
+ const value = Number(match[1]);
100
+ if (value > 0 && value <= 50) years.push(value);
101
+ }
102
+ }
103
+ return years.length ? Math.max(...years) : 0;
104
+ }
105
+
106
+ function extractName(text, fileName = "") {
107
+ const namePatterns = [
108
+ /(?:姓名|候选人)[::\s]*([\u4e00-\u9fa5]{2,5}|[A-Za-z][A-Za-z\s]{1,40})/,
109
+ /^([\u4e00-\u9fa5]{2,5})\s*$/m,
110
+ ];
111
+ for (const pattern of namePatterns) {
112
+ const match = text.match(pattern);
113
+ if (match && match[1]) return match[1].trim();
114
+ }
115
+ const fromFile = String(fileName || "").replace(/\.(pdf|docx?|txt|md)$/i, "").trim();
116
+ return fromFile || "未知候选人";
117
+ }
118
+
119
+ function extractEducation(text) {
120
+ const degreeOrder = ["博士", "硕士", "研究生", "本科", "大专", "专科", "高中"];
121
+ const degree = degreeOrder.find((item) => text.includes(item)) || "";
122
+ const schoolMatch = text.match(/([\u4e00-\u9fa5A-Za-z]{2,30}(?:大学|学院|学校|University|College))/);
123
+ return {
124
+ education: degree || "未识别",
125
+ school: schoolMatch ? schoolMatch[1] : "",
126
+ is_top_school: containsAny(text, TOP_SCHOOLS).length > 0,
127
+ };
128
+ }
129
+
130
+ function extractNumbers(text) {
131
+ const matches = text.match(/(?:提升|增长|降低|减少|节省|转化率|留存率|GMV|营收|成本|效率|用户|订单|访问量|QPS|DAU|MAU|ROI)?[^。;;\n]{0,18}\d+(?:\.\d+)?\s*(?:%|万|亿|k|K|人|次|元|天|小时|个月|年)?/g);
132
+ return unique(matches || []).slice(0, 20);
133
+ }
134
+
135
+ function extractCurrentPosition(text) {
136
+ const patterns = [
137
+ /(?:当前岗位|应聘岗位|职位|岗位)[::\s]*([^\n,。;;]{2,30})/,
138
+ /(工程师|开发|产品经理|项目经理|运营|销售|设计师|数据分析师|算法工程师|测试工程师|HR|招聘专员)/,
139
+ ];
140
+ for (const pattern of patterns) {
141
+ const match = text.match(pattern);
142
+ if (match && match[1]) return match[1].trim();
143
+ }
144
+ return "";
145
+ }
146
+
147
+ function parseJd({ job_description }) {
148
+ const text = normalizeText(job_description);
149
+ if (!text) throw new Error("job_description 不能为空");
150
+ const requiredSkills = unique(containsAny(text, SKILLS));
151
+ const requiredIndustry = unique(containsAny(text, INDUSTRIES));
152
+ const years = extractYears(text);
153
+ const preferredItems = [];
154
+ if (/985|211|双一流|名校/.test(text)) preferredItems.push("名校背景");
155
+ if (/大厂|头部公司|知名互联网|大型企业/.test(text)) preferredItems.push("大厂或大型企业经历");
156
+ if (/从0到1|高并发|海量|千万级|亿级|复杂系统/.test(text)) preferredItems.push("稀缺或复杂项目经历");
157
+ if (/管理|带团队|负责人|Leader|主管/.test(text)) preferredItems.push("团队管理或负责人经历");
158
+
159
+ const responsibilities = unique(
160
+ text
161
+ .split(/[。;;\n]/)
162
+ .map((line) => line.trim())
163
+ .filter((line) => line.length >= 8 && /(负责|参与|完成|推动|建设|设计|开发|运营|管理)/.test(line))
164
+ ).slice(0, 8);
165
+
166
+ return {
167
+ required_years: years,
168
+ required_skills: requiredSkills,
169
+ required_industry: requiredIndustry,
170
+ preferred_items: preferredItems,
171
+ responsibilities,
172
+ };
173
+ }
174
+
175
+ function parseResume({ resume }) {
176
+ const fileName = resume?.file_name || resume?.name || "";
177
+ const text = normalizeText(resume?.text);
178
+ if (!text) throw new Error("resume.text 不能为空");
179
+
180
+ const education = extractEducation(text);
181
+ const skills = unique(containsAny(text, SKILLS));
182
+ const industries = unique(containsAny(text, INDUSTRIES));
183
+ const verbs = unique(containsAny(text, POSITIVE_ACTIONS));
184
+ const numberClaims = extractNumbers(text);
185
+ const bigCompanies = unique(containsAny(text, BIG_COMPANIES));
186
+
187
+ return {
188
+ file_name: fileName,
189
+ name: extractName(text, fileName),
190
+ total_years: extractYears(text),
191
+ current_position: extractCurrentPosition(text),
192
+ education: education.education,
193
+ school: education.school,
194
+ is_top_school: education.is_top_school,
195
+ skills,
196
+ industries,
197
+ big_companies: bigCompanies,
198
+ work_experiences: extractWorkExperiences(text),
199
+ key_achievements: numberClaims.slice(0, 8),
200
+ all_verbs: verbs,
201
+ number_claims: numberClaims,
202
+ };
203
+ }
204
+
205
+ function extractWorkExperiences(text) {
206
+ const lines = text
207
+ .split(/\n+/)
208
+ .map((line) => line.trim())
209
+ .filter(Boolean);
210
+ const expLines = lines.filter((line) =>
211
+ /公司|工作经历|项目经历|实习|负责|主导|参与|任职|20\d{2}|19\d{2}/.test(line)
212
+ );
213
+ return expLines.slice(0, 12).map((line) => ({
214
+ raw: line.slice(0, 220),
215
+ duration_months: inferDurationMonths(line),
216
+ verbs: containsAny(line, POSITIVE_ACTIONS),
217
+ numbers: extractNumbers(line),
218
+ }));
219
+ }
220
+
221
+ function inferDurationMonths(line) {
222
+ const range = line.match(/(20\d{2}|19\d{2})[./年-]?\s*(\d{1,2})?.{0,6}(20\d{2}|19\d{2})?[./年-]?\s*(\d{1,2})?/);
223
+ if (!range) return 0;
224
+ const startYear = Number(range[1]);
225
+ const startMonth = Number(range[2] || 1);
226
+ const endYear = Number(range[3] || new Date().getFullYear());
227
+ const endMonth = Number(range[4] || 12);
228
+ const months = (endYear - startYear) * 12 + (endMonth - startMonth);
229
+ return months > 0 && months < 600 ? months : 0;
230
+ }
231
+
232
+ function matchResumeToJd(candidateInfo, jdInfo) {
233
+ const requiredYears = Number(jdInfo.required_years || 0);
234
+ let yearsScore = 100;
235
+ if (requiredYears > 0) {
236
+ yearsScore = clamp((Number(candidateInfo.total_years || 0) / requiredYears) * 100);
237
+ }
238
+
239
+ const requiredSkills = jdInfo.required_skills || [];
240
+ const matchedSkills = requiredSkills.filter((skill) =>
241
+ (candidateInfo.skills || []).some((candidateSkill) => candidateSkill.toLowerCase() === skill.toLowerCase())
242
+ );
243
+ const skillsScore = requiredSkills.length ? (matchedSkills.length / requiredSkills.length) * 100 : 70;
244
+
245
+ const requiredIndustry = jdInfo.required_industry || [];
246
+ const matchedIndustry = requiredIndustry.filter((industry) => (candidateInfo.industries || []).includes(industry));
247
+ const industryScore = requiredIndustry.length ? (matchedIndustry.length / requiredIndustry.length) * 100 : 70;
248
+
249
+ const matchScore = round1(yearsScore * 0.4 + skillsScore * 0.4 + industryScore * 0.2);
250
+ return {
251
+ match_score: matchScore,
252
+ detail: {
253
+ years_score: round1(yearsScore),
254
+ skills_score: round1(skillsScore),
255
+ industry_score: round1(industryScore),
256
+ required_years: requiredYears,
257
+ candidate_years: candidateInfo.total_years,
258
+ matched_skills: matchedSkills,
259
+ missing_skills: requiredSkills.filter((skill) => !matchedSkills.includes(skill)),
260
+ matched_industry: matchedIndustry,
261
+ },
262
+ conclusion:
263
+ matchScore >= 80 ? "岗位匹配度较高" :
264
+ matchScore >= 60 ? "岗位匹配度中等,建议结合面试核实" :
265
+ "岗位匹配度偏低",
266
+ };
267
+ }
268
+
269
+ function checkResumeTrust(candidateInfo, text) {
270
+ const flags = [];
271
+ let deduction = 0;
272
+
273
+ const exaggerations = unique(containsAny(text, EXAGGERATION_WORDS));
274
+ const exaggerationEvidence = exaggerations
275
+ .map((word) => evidenceAround(text, word))
276
+ .filter(Boolean)
277
+ .slice(0, 3);
278
+ if (exaggerations.length) {
279
+ const points = Math.min(24, exaggerations.length * 8);
280
+ deduction += points;
281
+ flags.push({
282
+ type: "疑似夸大表达",
283
+ deduction: points,
284
+ evidence: exaggerationEvidence,
285
+ reason: "出现强结论或高量级描述,建议结合项目细节核实。",
286
+ questions: ["请说明该成果的个人职责边界、数据来源和验证方式。"],
287
+ });
288
+ }
289
+
290
+ const vagueWords = unique(containsAny(text, VAGUE_WORDS));
291
+ if (vagueWords.length >= 2) {
292
+ deduction += 10;
293
+ flags.push({
294
+ type: "信息表述偏模糊",
295
+ deduction: 10,
296
+ evidence: vagueWords.slice(0, 4).map((word) => evidenceAround(text, word)).filter(Boolean),
297
+ reason: "存在较多泛化表述,缺少可验证的任务、范围或结果。",
298
+ questions: ["请举一个最能代表能力的具体项目,并说明目标、行动和结果。"],
299
+ });
300
+ }
301
+
302
+ if ((candidateInfo.skills || []).length >= 14 && (candidateInfo.number_claims || []).length <= 2) {
303
+ deduction += 10;
304
+ flags.push({
305
+ type: "技能堆砌信号",
306
+ deduction: 10,
307
+ evidence: [`识别到技能数量 ${candidateInfo.skills.length} 个,但量化成果较少。`],
308
+ reason: "技能列表较长但缺少成果支撑,建议核实真实熟练度。",
309
+ questions: ["请按熟练程度对简历中的技能排序,并说明最近一次实际使用场景。"],
310
+ });
311
+ }
312
+
313
+ if ((candidateInfo.total_years || 0) <= 2 && /高级|资深|专家|负责人|架构师/.test(text)) {
314
+ deduction += 12;
315
+ flags.push({
316
+ type: "年限与职级疑似不匹配",
317
+ deduction: 12,
318
+ evidence: [`识别工作年限约 ${candidateInfo.total_years || 0} 年,同时出现较高职级描述。`],
319
+ reason: "较短年限与高职级描述之间存在需核实之处。",
320
+ questions: ["请说明获得该职级或负责人角色的时间、团队规模和评估标准。"],
321
+ });
322
+ }
323
+
324
+ const trustScore = clamp(100 - deduction);
325
+ const trustLevel = trustScore >= 80 ? "高可信" : trustScore >= 60 ? "中可信" : "低可信";
326
+ return {
327
+ trust_score: trustScore,
328
+ trust_level: trustLevel,
329
+ trust_status: trustScore >= 60 ? "可进入排序" : "建议淘汰",
330
+ flags,
331
+ deduction_detail: flags.map((flag) => ({ type: flag.type, deduction: flag.deduction, reason: flag.reason })),
332
+ questions: flags.flatMap((flag) => flag.questions),
333
+ };
334
+ }
335
+
336
+ function scoreCompetitiveness(candidateInfo) {
337
+ const details = [];
338
+ let score = 50;
339
+
340
+ if ((candidateInfo.key_achievements || []).length >= 2) {
341
+ score += 10;
342
+ details.push({ item: "高含金量成就", points: 10, evidence: candidateInfo.key_achievements.slice(0, 3) });
343
+ }
344
+ if (/(高并发|大模型|推荐系统|风控|支付|搜索|分布式|复杂系统)/.test(JSON.stringify(candidateInfo))) {
345
+ score += 8;
346
+ details.push({ item: "稀缺经历", points: 8, evidence: ["识别到复杂系统或稀缺技术关键词"] });
347
+ }
348
+ if (candidateInfo.is_top_school) {
349
+ score += 5;
350
+ details.push({ item: "名校背景", points: 5, evidence: [candidateInfo.school || "简历包含名校相关描述"] });
351
+ }
352
+ if ((candidateInfo.big_companies || []).length) {
353
+ score += 5;
354
+ details.push({ item: "大厂背景", points: 5, evidence: candidateInfo.big_companies.slice(0, 3) });
355
+ }
356
+ if (/初级|中级|高级|资深|负责人|主管|经理|总监/.test(JSON.stringify(candidateInfo))) {
357
+ score += 5;
358
+ details.push({ item: "职级递进", points: 5, evidence: ["简历包含职级或职责递进描述"] });
359
+ }
360
+
361
+ return { competitiveness_score: clamp(score), bonus_details: details };
362
+ }
363
+
364
+ function recommendationLabel(trustScore, finalScore) {
365
+ if (trustScore < 60) return "淘汰";
366
+ if (trustScore >= 80 && finalScore >= 70) return "优先联系";
367
+ if (trustScore >= 80 && finalScore >= 50) return "可推进面试";
368
+ if (trustScore >= 60 && finalScore >= 70) return "建议核实(能力优秀,重点追问疑点)";
369
+ if (trustScore >= 60 && finalScore >= 50) return "建议核实";
370
+ return "暂不联系";
371
+ }
372
+
373
+ function analyzeCandidate({ resume, job_description = "", jd_info = null, config = {} }) {
374
+ const mergedConfig = { ...DEFAULT_CONFIG, ...(config || {}) };
375
+ const text = normalizeText(resume?.text);
376
+ if (!text) throw new Error("resume.text 不能为空");
377
+ const candidateInfo = parseResume({ resume });
378
+ const jdInfo = jd_info || (job_description ? parseJd({ job_description }) : null);
379
+ const matchResult = jdInfo ? matchResumeToJd(candidateInfo, jdInfo) : null;
380
+ const checkResult = checkResumeTrust(candidateInfo, text);
381
+ const competitiveness = scoreCompetitiveness(candidateInfo);
382
+
383
+ let finalScore = null;
384
+ let label = checkResult.trust_score < mergedConfig.trust_threshold ? "淘汰" : "待补充 JD 后评分";
385
+ if (matchResult && checkResult.trust_score >= mergedConfig.trust_threshold) {
386
+ finalScore = round1(
387
+ matchResult.match_score * mergedConfig.match_weight +
388
+ competitiveness.competitiveness_score * mergedConfig.competitiveness_weight
389
+ );
390
+ label = recommendationLabel(checkResult.trust_score, finalScore);
391
+ }
392
+
393
+ return {
394
+ candidate_info: candidateInfo,
395
+ match_result: matchResult,
396
+ check_result: checkResult,
397
+ competitiveness,
398
+ final_score: finalScore,
399
+ recommendation_label: label,
400
+ };
401
+ }
402
+
403
+ function screenResumes({ resumes, job_description, config = {} }) {
404
+ if (!Array.isArray(resumes) || resumes.length === 0) throw new Error("resumes 必须是非空数组");
405
+ if (!normalizeText(job_description)) throw new Error("job_description 不能为空");
406
+
407
+ const mergedConfig = { ...DEFAULT_CONFIG, ...(config || {}) };
408
+ const jdInfo = parseJd({ job_description });
409
+ const analyzed = resumes.map((resume) => analyzeCandidate({
410
+ resume,
411
+ jd_info: jdInfo,
412
+ config: mergedConfig,
413
+ }));
414
+
415
+ const eliminated = analyzed
416
+ .filter((item) => item.check_result.trust_score < mergedConfig.trust_threshold)
417
+ .map((item) => ({
418
+ candidate: item.candidate_info.name,
419
+ file_name: item.candidate_info.file_name,
420
+ trust_score: item.check_result.trust_score,
421
+ reason: item.check_result.flags.map((flag) => flag.type).join(";") || "可信度低于阈值",
422
+ evidence: item.check_result.flags.flatMap((flag) => flag.evidence).slice(0, 5),
423
+ }));
424
+
425
+ const rankedCandidates = analyzed
426
+ .filter((item) => item.check_result.trust_score >= mergedConfig.trust_threshold)
427
+ .sort((a, b) => {
428
+ if ((b.final_score || 0) !== (a.final_score || 0)) return (b.final_score || 0) - (a.final_score || 0);
429
+ return b.check_result.trust_score - a.check_result.trust_score;
430
+ })
431
+ .map((item, index) => ({
432
+ rank: index + 1,
433
+ candidate: item.candidate_info.name,
434
+ file_name: item.candidate_info.file_name,
435
+ total_years: item.candidate_info.total_years,
436
+ final_score: item.final_score,
437
+ match_score: item.match_result?.match_score,
438
+ trust_score: item.check_result.trust_score,
439
+ trust_level: item.check_result.trust_level,
440
+ recommendation_label: item.recommendation_label,
441
+ matched_skills: item.match_result?.detail?.matched_skills || [],
442
+ missing_skills: item.match_result?.detail?.missing_skills || [],
443
+ }));
444
+
445
+ const result = {
446
+ version: SERVER_VERSION,
447
+ config: mergedConfig,
448
+ jd_info: jdInfo,
449
+ eliminated,
450
+ ranked_candidates: rankedCandidates,
451
+ risk_questions: analyzed.map((item) => ({
452
+ candidate: item.candidate_info.name,
453
+ questions: item.check_result.questions,
454
+ flags: item.check_result.flags,
455
+ })).filter((item) => item.questions.length || item.flags.length),
456
+ bonus_details: analyzed.map((item) => ({
457
+ candidate: item.candidate_info.name,
458
+ details: item.competitiveness.bonus_details,
459
+ })).filter((item) => item.details.length),
460
+ raw_analysis: analyzed,
461
+ };
462
+ result.summary = `共分析 ${analyzed.length} 份简历,晋级 ${rankedCandidates.length} 人,淘汰 ${eliminated.length} 人。`;
463
+ result.markdown_report = generateReport({ screening_result: result }).markdown;
464
+ return result;
465
+ }
466
+
467
+ function generateReport({ screening_result }) {
468
+ const result = screening_result || {};
469
+ const eliminated = result.eliminated || [];
470
+ const ranked = result.ranked_candidates || [];
471
+ const riskQuestions = result.risk_questions || [];
472
+ const bonusDetails = result.bonus_details || [];
473
+
474
+ const lines = [];
475
+ lines.push("## 淘汰名单(可信度低于阈值,不参与排序)");
476
+ if (!eliminated.length) {
477
+ lines.push("暂无。");
478
+ } else {
479
+ lines.push("| 候选人 | 可信度 | 淘汰理由 |");
480
+ lines.push("|---|---:|---|");
481
+ for (const item of eliminated) {
482
+ lines.push(`| ${item.candidate} | ${item.trust_score} | ${item.reason || "可信度低于阈值"} |`);
483
+ }
484
+ }
485
+
486
+ lines.push("");
487
+ lines.push("## 晋级排序榜(按综合得分从高到低)");
488
+ if (!ranked.length) {
489
+ lines.push("暂无。");
490
+ } else {
491
+ lines.push("| 排名 | 候选人 | 综合得分 | 匹配度 | 可信度 | 建议标签 |");
492
+ lines.push("|---:|---|---:|---:|---:|---|");
493
+ for (const item of ranked) {
494
+ lines.push(`| ${item.rank} | ${item.candidate}(${item.total_years || 0}年) | ${item.final_score ?? "-"} | ${item.match_score ?? "-"} | ${item.trust_score} | ${item.recommendation_label} |`);
495
+ }
496
+ }
497
+
498
+ lines.push("");
499
+ lines.push("## 疑点与追问清单");
500
+ if (!riskQuestions.length) {
501
+ lines.push("暂无明显疑点。");
502
+ } else {
503
+ for (const item of riskQuestions) {
504
+ lines.push(`### ${item.candidate}`);
505
+ for (const flag of item.flags || []) {
506
+ const evidence = (flag.evidence || []).join(";") || "无明确原文片段";
507
+ const question = (flag.questions || []).join(";") || "建议面试核实相关经历。";
508
+ lines.push(`- ${evidence} -> ${flag.type} -> ${flag.reason} -> ${question}`);
509
+ }
510
+ }
511
+ }
512
+
513
+ lines.push("");
514
+ lines.push("## 竞争加分明细");
515
+ if (!bonusDetails.length) {
516
+ lines.push("暂无明确竞争加分项。");
517
+ } else {
518
+ for (const item of bonusDetails) {
519
+ lines.push(`### ${item.candidate}`);
520
+ for (const detail of item.details || []) {
521
+ lines.push(`- ${detail.item}:+${detail.points},依据:${(detail.evidence || []).join(";")}`);
522
+ }
523
+ }
524
+ }
525
+
526
+ lines.push("");
527
+ lines.push("## 评估说明");
528
+ lines.push("- 本分析基于简历文本特征与规则引擎生成,仅供招聘初筛参考,不构成对候选人品行或录用结果的定性。");
529
+ lines.push("- 关键数据请以面试核实、作品验证和背景调查为准。");
530
+
531
+ return { markdown: lines.join("\n") };
532
+ }
533
+
534
+ const TOOLS = [
535
+ {
536
+ name: "parse_jd",
537
+ description: "从岗位 JD 文本中提取工作年限、技能、行业、职责和加分项要求。",
538
+ inputSchema: {
539
+ type: "object",
540
+ properties: {
541
+ job_description: { type: "string", description: "岗位 JD 原文。" },
542
+ },
543
+ required: ["job_description"],
544
+ },
545
+ },
546
+ {
547
+ name: "analyze_candidate",
548
+ description: "分析单个候选人,输出结构化简历信息、可信度、匹配度、综合得分和面试追问。",
549
+ inputSchema: {
550
+ type: "object",
551
+ properties: {
552
+ resume: {
553
+ type: "object",
554
+ properties: {
555
+ file_name: { type: "string" },
556
+ text: { type: "string" },
557
+ },
558
+ required: ["text"],
559
+ },
560
+ job_description: { type: "string" },
561
+ config: { type: "object" },
562
+ },
563
+ required: ["resume"],
564
+ },
565
+ },
566
+ {
567
+ name: "screen_resumes",
568
+ description: "批量分析多份简历和岗位 JD,输出淘汰名单、晋级排序榜、疑点追问和 Markdown 报告。",
569
+ inputSchema: {
570
+ type: "object",
571
+ properties: {
572
+ resumes: {
573
+ type: "array",
574
+ items: {
575
+ type: "object",
576
+ properties: {
577
+ file_name: { type: "string" },
578
+ text: { type: "string" },
579
+ },
580
+ required: ["text"],
581
+ },
582
+ },
583
+ job_description: { type: "string" },
584
+ config: {
585
+ type: "object",
586
+ properties: {
587
+ trust_threshold: { type: "number" },
588
+ match_weight: { type: "number" },
589
+ competitiveness_weight: { type: "number" },
590
+ },
591
+ },
592
+ },
593
+ required: ["resumes", "job_description"],
594
+ },
595
+ },
596
+ {
597
+ name: "generate_report",
598
+ description: "把 screen_resumes 的结构化结果转换为中文 Markdown 报告。",
599
+ inputSchema: {
600
+ type: "object",
601
+ properties: {
602
+ screening_result: { type: "object" },
603
+ },
604
+ required: ["screening_result"],
605
+ },
606
+ },
607
+ ];
608
+
609
+ function callTool(name, args) {
610
+ if (name === "parse_jd") return parseJd(args || {});
611
+ if (name === "analyze_candidate") return analyzeCandidate(args || {});
612
+ if (name === "screen_resumes") return screenResumes(args || {});
613
+ if (name === "generate_report") return generateReport(args || {});
614
+ throw new Error(`未知工具:${name}`);
615
+ }
616
+
617
+ let inputBuffer = Buffer.alloc(0);
618
+
619
+ process.stdin.on("data", (chunk) => {
620
+ inputBuffer = Buffer.concat([inputBuffer, chunk]);
621
+ processMessages();
622
+ });
623
+
624
+ process.stdin.on("error", () => process.exit(1));
625
+
626
+ function processMessages() {
627
+ while (true) {
628
+ const headerEnd = inputBuffer.indexOf("\r\n\r\n");
629
+ if (headerEnd === -1) return;
630
+
631
+ const header = inputBuffer.slice(0, headerEnd).toString("utf8");
632
+ const match = header.match(/Content-Length:\s*(\d+)/i);
633
+ if (!match) {
634
+ inputBuffer = inputBuffer.slice(headerEnd + 4);
635
+ continue;
636
+ }
637
+
638
+ const length = Number(match[1]);
639
+ const messageStart = headerEnd + 4;
640
+ const messageEnd = messageStart + length;
641
+ if (inputBuffer.length < messageEnd) return;
642
+
643
+ const raw = inputBuffer.slice(messageStart, messageEnd).toString("utf8");
644
+ inputBuffer = inputBuffer.slice(messageEnd);
645
+
646
+ try {
647
+ handleMessage(JSON.parse(raw));
648
+ } catch (error) {
649
+ sendError(null, -32700, `解析 MCP 消息失败:${error.message}`);
650
+ }
651
+ }
652
+ }
653
+
654
+ function handleMessage(message) {
655
+ if (!message || typeof message !== "object") return;
656
+ if (message.id === undefined || message.id === null) return;
657
+
658
+ try {
659
+ if (message.method === "initialize") {
660
+ sendResult(message.id, {
661
+ protocolVersion: message.params?.protocolVersion || "2024-11-05",
662
+ capabilities: { tools: {} },
663
+ serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
664
+ });
665
+ return;
666
+ }
667
+
668
+ if (message.method === "tools/list") {
669
+ sendResult(message.id, { tools: TOOLS });
670
+ return;
671
+ }
672
+
673
+ if (message.method === "tools/call") {
674
+ const toolName = message.params?.name;
675
+ const args = message.params?.arguments || {};
676
+ const result = callTool(toolName, args);
677
+ sendResult(message.id, {
678
+ content: [
679
+ {
680
+ type: "text",
681
+ text: JSON.stringify(result, null, 2),
682
+ },
683
+ ],
684
+ structuredContent: result,
685
+ });
686
+ return;
687
+ }
688
+
689
+ sendError(message.id, -32601, `不支持的方法:${message.method}`);
690
+ } catch (error) {
691
+ sendError(message.id, -32000, error.message || String(error));
692
+ }
693
+ }
694
+
695
+ function sendResult(id, result) {
696
+ writeMessage({ jsonrpc: "2.0", id, result });
697
+ }
698
+
699
+ function sendError(id, code, message) {
700
+ writeMessage({ jsonrpc: "2.0", id, error: { code, message } });
701
+ }
702
+
703
+ function writeMessage(payload) {
704
+ const json = JSON.stringify(payload);
705
+ const bytes = Buffer.byteLength(json, "utf8");
706
+ process.stdout.write(`Content-Length: ${bytes}\r\n\r\n${json}`);
707
+ }
@@ -0,0 +1,73 @@
1
+ {
2
+ "serviceName": "简历初筛MCP",
3
+ "description": "为简历初筛执行器提供岗位JD解析、简历结构化、岗位匹配度计算、可信度检测、综合评分、候选人排序和中文报告生成能力。Agent负责对话、文件接收和流程调度,MCP负责稳定的规则计算和结构化输出,避免仅依赖提示词进行打分和排序。",
4
+ "recommendedInstallMethod": "npx",
5
+ "npxPublishedPackage": {
6
+ "mcpServers": {
7
+ "resume-screening-mcp": {
8
+ "command": "npx",
9
+ "args": [
10
+ "-y",
11
+ "resume-screening-mcp"
12
+ ],
13
+ "env": {}
14
+ }
15
+ }
16
+ },
17
+ "npxGitHubPackage": {
18
+ "mcpServers": {
19
+ "resume-screening-mcp": {
20
+ "command": "npx",
21
+ "args": [
22
+ "-y",
23
+ "github:你的用户名/resume-screening-mcp"
24
+ ],
25
+ "env": {}
26
+ }
27
+ }
28
+ },
29
+ "uvxNotRecommended": {
30
+ "note": "当前项目是 Node.js MCP,不是 Python 包,不建议选择 uvx。除非后续重写为 Python 包。",
31
+ "mcpServers": {
32
+ "resume-screening-mcp": {
33
+ "command": "uvx",
34
+ "args": [
35
+ "resume-screening-mcp"
36
+ ],
37
+ "env": {}
38
+ }
39
+ }
40
+ },
41
+ "sseNotRecommendedUntilDeployed": {
42
+ "note": "只有部署成远程 SSE MCP 服务后才使用。",
43
+ "mcpServers": {
44
+ "resume-screening-mcp": {
45
+ "url": "https://你的域名/sse",
46
+ "env": {}
47
+ }
48
+ }
49
+ },
50
+ "streamableHttpNotRecommendedUntilDeployed": {
51
+ "note": "只有部署成远程 Streamable HTTP MCP 服务后才使用。",
52
+ "mcpServers": {
53
+ "resume-screening-mcp": {
54
+ "url": "https://你的域名/mcp",
55
+ "env": {}
56
+ }
57
+ }
58
+ },
59
+ "componentLibraryNotRecommended": {
60
+ "note": "组件库适合平台内置组件,不适合当前自定义简历筛选 MCP。"
61
+ },
62
+ "localNodeTest": {
63
+ "mcpServers": {
64
+ "resume-screening-mcp": {
65
+ "command": "node",
66
+ "args": [
67
+ "C:\\Users\\Lenovo\\Documents\\Codex\\2026-09-18\\ge-m\\outputs\\resume-screening-mcp\\src\\index.js"
68
+ ],
69
+ "env": {}
70
+ }
71
+ }
72
+ }
73
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@cjsqbn/resume-screening-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP service for resume screening, JD matching, trust checking, scoring and candidate ranking.",
5
+ "type": "module",
6
+ "bin": "./index.js",
7
+ "files": [
8
+ "index.js",
9
+ "README.md",
10
+ "agent-prompt.md",
11
+ "platform-ready.md",
12
+ "mcp-config-examples.json"
13
+ ],
14
+ "scripts": {
15
+ "start": "node ./index.js",
16
+ "test:smoke": "node ./smoke-test.js"
17
+ },
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "license": "MIT"
25
+ }
@@ -0,0 +1,195 @@
1
+ # 简历初筛 MCP 平台填写版
2
+
3
+ ## 一 服务名称
4
+
5
+ ```text
6
+ 简历初筛MCP
7
+ ```
8
+
9
+ 说明:平台服务名称限制 30 字以内,这个名称简短、明确,适合直接填写。
10
+
11
+ ## 二 服务描述
12
+
13
+ ```text
14
+ 为简历初筛执行器提供岗位JD解析、简历结构化、岗位匹配度计算、可信度检测、综合评分、候选人排序和中文报告生成能力。Agent负责对话、文件接收和流程调度,MCP负责稳定的规则计算和结构化输出,避免仅依赖提示词进行打分和排序。
15
+ ```
16
+
17
+ ## 三 推荐安装方式
18
+
19
+ 推荐选择:
20
+
21
+ ```text
22
+ npx
23
+ ```
24
+
25
+ 原因:当前 MCP 是 Node.js 标准 stdio MCP 服务,最适合通过 `npx` 或 `node` 启动。
26
+
27
+ ## 四 各安装方式说明
28
+
29
+ | 安装方式 | 是否推荐 | 说明 |
30
+ |---|---|---|
31
+ | npx | 推荐 | 当前项目是 Node MCP,最适合使用 npx。发布到 npm 或 GitHub 后可以直接配置。 |
32
+ | uvx | 不推荐 | uvx 适合 Python MCP 包。本项目不是 Python 包,除非后续重写为 Python 版本。 |
33
+ | sse | 暂不推荐 | 适合已经部署成远程 SSE 服务的 MCP。本项目当前是本地 stdio 服务。 |
34
+ | streamableHttp | 暂不推荐 | 适合部署成远程 HTTP MCP 服务。本项目当前没有远程 HTTP 地址。 |
35
+ | 组件库 | 不推荐 | 适合平台内置组件,不适合这个自定义简历筛选服务。 |
36
+
37
+ ## 五 MCP 服务配置
38
+
39
+ ### 方案 A 本地测试配置
40
+
41
+ 如果平台允许使用 `node` 命令,可以直接填这个:
42
+
43
+ ```json
44
+ {
45
+ "mcpServers": {
46
+ "resume-screening-mcp": {
47
+ "command": "node",
48
+ "args": [
49
+ "C:\\Users\\Lenovo\\Documents\\Codex\\2026-09-18\\ge-m\\outputs\\resume-screening-mcp\\src\\index.js"
50
+ ],
51
+ "env": {}
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ ### 方案 B 发布到 npm 后的 npx 配置
58
+
59
+ 如果你把项目发布到 npm,选择 `npx`,配置填写:
60
+
61
+ ```json
62
+ {
63
+ "mcpServers": {
64
+ "resume-screening-mcp": {
65
+ "command": "npx",
66
+ "args": [
67
+ "-y",
68
+ "resume-screening-mcp"
69
+ ],
70
+ "env": {}
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ ### 方案 C 上传到 GitHub 后的 npx 配置
77
+
78
+ 如果你暂时不发布 npm,可以先上传到 GitHub,然后配置:
79
+
80
+ ```json
81
+ {
82
+ "mcpServers": {
83
+ "resume-screening-mcp": {
84
+ "command": "npx",
85
+ "args": [
86
+ "-y",
87
+ "github:你的用户名/resume-screening-mcp"
88
+ ],
89
+ "env": {}
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ 使用时把 `你的用户名` 改成你的 GitHub 用户名。
96
+
97
+ ### 方案 D uvx 配置说明
98
+
99
+ 当前不建议填写 uvx。如果平台必须选择 uvx,需要先把 MCP 改写成 Python 包,然后才可以使用类似配置:
100
+
101
+ ```json
102
+ {
103
+ "mcpServers": {
104
+ "resume-screening-mcp": {
105
+ "command": "uvx",
106
+ "args": [
107
+ "resume-screening-mcp"
108
+ ],
109
+ "env": {}
110
+ }
111
+ }
112
+ }
113
+ ```
114
+
115
+ 当前生成的项目不是 Python 包,所以不要直接使用这个配置。
116
+
117
+ ### 方案 E sse 配置说明
118
+
119
+ 当前不建议选择 sse。只有当你把 MCP 部署成远程 SSE 服务后,才使用类似配置:
120
+
121
+ ```json
122
+ {
123
+ "mcpServers": {
124
+ "resume-screening-mcp": {
125
+ "url": "https://你的域名/sse",
126
+ "env": {}
127
+ }
128
+ }
129
+ }
130
+ ```
131
+
132
+ ### 方案 F streamableHttp 配置说明
133
+
134
+ 当前不建议选择 streamableHttp。只有当你把 MCP 部署成远程 HTTP 服务后,才使用类似配置:
135
+
136
+ ```json
137
+ {
138
+ "mcpServers": {
139
+ "resume-screening-mcp": {
140
+ "url": "https://你的域名/mcp",
141
+ "env": {}
142
+ }
143
+ }
144
+ }
145
+ ```
146
+
147
+ ## 六 当前 MCP 工具
148
+
149
+ | 工具名 | 作用 |
150
+ |---|---|
151
+ | `parse_jd` | 解析岗位 JD,提取年限、技能、行业、职责和加分项 |
152
+ | `analyze_candidate` | 分析单个候选人,输出结构化信息、可信度、匹配度和综合得分 |
153
+ | `screen_resumes` | 批量分析多份简历,输出淘汰名单、晋级榜、疑点追问和报告 |
154
+ | `generate_report` | 将结构化筛选结果转换为中文 Markdown 报告 |
155
+
156
+ ## 七 推荐填写总结
157
+
158
+ 如果你的平台页面和截图一致,建议这样填:
159
+
160
+ 服务名称:
161
+
162
+ ```text
163
+ 简历初筛MCP
164
+ ```
165
+
166
+ 描述:
167
+
168
+ ```text
169
+ 为简历初筛执行器提供岗位JD解析、简历结构化、岗位匹配度计算、可信度检测、综合评分、候选人排序和中文报告生成能力。Agent负责对话、文件接收和流程调度,MCP负责稳定的规则计算和结构化输出,避免仅依赖提示词进行打分和排序。
170
+ ```
171
+
172
+ 安装方式:
173
+
174
+ ```text
175
+ npx
176
+ ```
177
+
178
+ MCP 服务配置:
179
+
180
+ ```json
181
+ {
182
+ "mcpServers": {
183
+ "resume-screening-mcp": {
184
+ "command": "npx",
185
+ "args": [
186
+ "-y",
187
+ "github:你的用户名/resume-screening-mcp"
188
+ ],
189
+ "env": {}
190
+ }
191
+ }
192
+ }
193
+ ```
194
+
195
+ 如果你还没有上传 GitHub,就先不要用 GitHub 配置;先使用本地 `node` 配置测试。