Skip to main content
Glama
Instahyre MCP

Instahyre MCP

by Sundeepg98

Instahyre MCP

Instahyre 作为 MCP 工具:公开的职位搜索、本平台真正核心的已认证入站侧、消息收件箱,以及受保护的简历写入。

Instahyre 是一个逆向市场——雇主把你安置在一个经过筛选的队列中,招聘方会打开你的简历。出站搜索只是商品化的一半;稀缺的信号是谁与你互动了,以及你多快能察觉到。instahyre_inbound_digest 就是以一次调用回答这个问题的工具。

架构:默认 httpx,API 无法触及之处使用浏览器

47 个工具中有 44 个是纯 httpx。三个工具使用浏览器,而这三个工具的说明都写明了这一点。

Instahyre 的 /api/v1/* 免受 Cloudflare 机器人管理的限制。它会直接在第一次请求时回应一个冷启动、未认证、诚实标识的 HTTP 客户端——不需要 cookie、不需要 token、也不用经过任何 JS 验证。我在约 ~300 次实时请求上实测:使用默认的 python-httpx User-Gent,零限制、零验证挑战。因此 API 就是数据路径,将来也是。

它的 HTML 页面则另当别论:那些页面确实受 Cloudflare 门户控制(对 httpx 返回 403,对无头 Chromium 显示 "Just a moment..."——两者均实测过)。当某内容只存在于这些页面时,真实浏览器是我读取它的诚实方式,而本服务器刻意使用浏览器,并不是假装那个内容不可达。

这个二分心是在付出代价后学来的。更早的版本把规则定成“永远不用浏览器”,并借此将消息正文报为不可能。它们从来就是可能的。会话列表只是位于一个没人想到过的命名空间——/ob page/candidate_conversation,而非消息资源所住的 /resume_modal/emails/ 命名空间。浏览器通过记录收件箱播了什么,在一次页面加载中就找到了它。之后断定,端点其实是普普通通的 /api/v1/*,所以使用它的工具也全都是纯 httpx。浏览器回答了一个无法问 API 的问题;它并没有成为数据路径的一部分。

工具

使用浏览器?

为什么

instahyre_login_browser

是,可见窗口

Google OAuth 是一段任何 HTTP 客户端都不能完成的跳转、洪舞。

instahyre_verify_apply_target

是,可见窗口

读取一个服务端注入的页面标志,它决定应用会把申请提交到哪个端点。没有 API 暴露它,而该页面受 Cloudflare 门户控。申请不可撤回,因此在这里用一个浏览器而不靠猜测,是值得的。

instahyre_reauth

是,无头模式

重新收割该持久资料自身、长期有效的 session_id,它比磁盘上存的那份更持久。它只加载 /candidate/opportunities/——绝不是登录页:一个声称“这不是登录”的工具,不应去请求登录 URL;让带着真实会话的浏览器去访问登录页,也没必要冒这种险。无头模式是保障,不是取舍:无窗口,就没有可等的人。

其他 44 个工具

否

纯 httpx。

这两个 可见窗口 的浏览器工具会在路由处拒绝每一个非 GET 请求,除了 Cloudflare 自己的 /cdn-cgi/ 挑战握手——它不会改动账户任何内容,没有它,验证就永远无法完成。二者都不会点击任何东西。

这里与姊妹端 Naukari 服务器之间还是保留了有意的差异;Naukari 需要一个持久 Chrome 配置文件、CDP 桥,以及一千四百行左右的 data path 反 bot 管道。而这个项目里,浏览器是偶发的、选进的。

我们不装浏览器 User-Agent。 它什么都不买(API 接受这个诚实的标识),而且 Instahyre 的条款也不许伪造 headers。若哪天 API 的这个豁免被移除,客户端就会抛出一个 ChallengeDetected 类型错误——这是让你停下来并重新评估的信号,而不是绕过它的信号。

Related MCP server: MCP Job-Tracker Server

安装

python -m venv venv
venv/Scripts/python -m pip install -r requirements.txt

# ...then jobcore, the shared scoring engine. It is REQUIRED, and it is not on
# PyPI. Pick the line that matches your checkout:
venv/Scripts/python -m pip install -e ../jobcore          # you have the sibling
venv/Scripts/python -m pip install -r requirements-ci.txt # you do not (pinned, from git)

venv/Scripts/python -m pytest tests/ -q

The two jobcore lines are alternatives, not steps. Run the editable one if ../jobcore is 出库 on the side of this repo——这是迭代评分器唯一的方法,而 requirements-ci.txt 那行则用在其他情况。不要 order the second after the first: direct URL requirement 会静默卸载 editable 安装,而不会有 any "already satisfied" 行来提醒你。

Playwright 的浏览器二进制只有这三个浏览器工具(instahyre_login_browser、instahyre_verify_apply_target、instahyre_reauth)需要。其他 44 个不依赖它,instahyre_reauth 也会用提示 “no silent renew was possible” 并给出回淘方案而不是抛错,因此无 chromium 的检出只是功能下降,不会坏掉:

venv/Scripts/python -m playwright install chromium

工具

做有联系的工作。

公开 —— 无需登录

工具

做什么

请求数

instahyre_search_jobs

搜索实时职位。可用筛选项:技能、职位类型、地点、公司、行业、规模、经历、职位。

1

instahyre_get_job

单个职位唯一的描述、经验带、招聘者、机构判定。

1(缓存 6h)

instahyre_get_company

雇主 profile 加上他们当前所有活动职位。同时是一个“公司是否在平台”的可 oracle。

1

instahyre_market_stats

给定的区块的刻画市場总览,且在这份数据全无职位记录。

1

instahyre_rank_jobs

搜索,然后照与你自身技能匹配度排列成页。技能来自 my_skills,没有则来自共享配置,再没有,并且你在登录时就用你自己 Instahyre 的 profile——skills_source 标明哪个生效。

1+(使用 profile 回退时 +2)

instahyre_sync_index

将一份数据分页分页导入本地索引,并报告从上次以来哪些是 NEW。

1/page

instahyre_list_job_functions

58 个职位职能及其 id。

1(缓存 30d)

instahyre_list_locations

合法的位置 token,并已分组。

1(缓存 30d)

instahyre_list_industries

74 个行业的类型和 id。

1(缓存 30d)

instahyre_server_info

缓存状态、请求数,以及这个平台无法提供的内容。

0

instahyre_config

现生效的评分策略、其哈希,以及它来自哪个文件。

0

已认证 —— 入站分类

The half that matters is the one that matters。

工具

说明

请求

instahyre_inbound_digest

从这里开始读。 队列徽章、谁看过你的简历、未读消息、得分最高但尚未被触及的匹配,以及上次运行以来新出现的内容。

~5

instahyre_list_opportunities

被策展的队列:雇主为你匹配的职位,带真实匹配分数。在全队列中统一排序。

1

instahyre_get_opportunity

单个匹配,由队列记录 + 公开职位详情 + 该雇主的同类职位组合而成。

2-3

instahyre_activity_counts

队列的切面:状态、地点、行业、雇主、规模。不含记录。

1

instahyre_recruiter_activeity

谁查看过 / 联系过 / 没有把你列入候选名单。这是这里最易消逝的信号。

2

instahyre_list_applications

每一份申请和婉拒,附状态。

2

instahyre_get_profile

你的个人资料,加上按每一项缺口所costs的代价排定的完整性缺口。

1-2

instahyre_account_settings

可见性、通知、屏蔽的雇主。

1-2

instahyre_apply

申请一个机会。不可逆。 除非 confirm=True 否则只能预览。

0-1

instahyre_decline_opportunity

将一个标记为“不考虑”。同样不可逆,同一样门阚。

0-1

收件箱——读取,以及恰好一个写入

招聘者的对话和消息正文。每个 READ 在发出前都会对照一个“会变更”的路径片段清单来做检查,所以读取层不能发送、标星或标记已读——send_message 仍然在那个清单上,读取端也仍然拒绝它。

回复走另一扇门:一个 allowlist,里面只有恰好一个 URL。部分发消息、标星、已读、批量全读,不仅被拒绝,它们连构造它们的分支都根本不存在。

工具

说明

请求

instahyre_list_conversations

会话列表,附带从职位合并进来的公司和职位。过滤器:状态、未读、标的星、任意文本。

1(若 include_job 则每个职位 +1)

instahyre_read_conversation

单个会话中的每条消息,以文本、最早的置前。一个不属于他的 conv_id 会抛出 not_found,而不是返回空线程。

若线程返回为空则 +1

instahyre_inbox_counts

未读 / 已标星 / 已标星且未读的数量合计。

1

instahyre_reply_to_conversation

往一个会话发送一条回复。无法逆转——必须 confirm=True。预览会显示服务器报告的收件人、公司、职位以及逐字正文。

预览 2-3,真正发送 4-5

一个诚实的中文提示,写在工具自己的 docstring 里: „读取“一个会话”可能在 Instahyre 的那一边把它标记为“已读”。 该网不发送“标记为”已读的消息——ii 只是在本地减掉 badge 计数——但这种只有当 server 在取回消息时就真的做了标记,才自洽。列会话和计数值是可以证明不会产生修改的;但取一个会话并不能被证明没有产生修改。这项没有被测试,因为收件箱当前没有会话。

写入 —— 这些会更改你的账号

工具

作用

请求

instahyre_update_skills

增加技能。只增,先快照,然后验证,除非 confirm=True 否则只预览。

2-4

instahyre_update_profile

设置职位、公司或经验年限。同一个门限。

2-4

instahyre_restore_profile

把技能列表放回到某个快照。

2-3

instahyre_list_profile_showshots

磁盘上的恢复点。

0

instahyre_verify_apply_target

打开浏览器。 重新测量一次申请实际上会 POST 到的终结点。

0(浏览器)

捕获写入 —— 建设之前就测量过

该层中的每条请求都是在代码写出来之前录成的。另有五个写入面在 2026-08-手工合成并被当场拒绝——because 其中每一个都没有已经录下的 request body;而且用猜的 body 发起写入,反而不如不加任何工具。猜错的通常就无害地 400 一下;而猜对了一半的,则会成功,且做一个没人选的操作,这种事在这个平台上为永久的。

scripts/capture_rite_contracts.py 就是解除这种方式。它打开真实已登录的浏览器,在路由层上中止所有非 GET 请求,并记录被中止了什么——方法、URL、body、headers。它只会在 s 从页面内部发起一个 POST 并看到 路由去阻断它之后才会继续走,所以“没有发出任何请求”就是一项实际测量,而不是假设。

工具

做了什么

契约测量值

instahye_support_tiket

提交其工单。有人来读,无法删改,除非 confirm=True。

Wire —— 记录并在路由层中断

instahye_toggle_job_alert

打开/关闭某个已存搜索结果对的提醒。可逆。根据标志位把查询词串也发送了,像网站原样。

已发布源码中的整个函数

instahye_referal_link

要自己的内推链接。不联系任何人。

已发布的源码

instahye_referal_contacts

复查 Instح 会把哪些人当作可邀请列表。一个读取 — 在其客户端也是 GET。

已发布源码

instahye_send_referal_invites

邀请某人。不可逆。 预览会列出每个要求收件列表的人。

已发布的源码

instahye_s_end_referral_invites 是真正有后果的那个:收件邮件里带着他的姓名,它会到达认识他的人那里,而 Insta宇并没有在它的任何产品中提供“撤回”。所以 confirm=false 打印完整收件列表;格式错误地址是拒绝,不会去送;重复的收件数在统计之前会先去除;一次调用最多发不超过十人。

两个表层仍然没有 built。原因就出现在 constants.UNVERIFIED_WRITE_SURFACES 中。 筛审问卷面只剩在真实机会上按下 Apply“才能打才打 ———而这个服务器唯一永远不能有的行为,就是“按下真实机会的 Apply”;所以捕获技术就被它要服务的规则所阻断id>。The workex PUT 在任何发送的包里部都没有调用处,在登录后profile 页面也没有任何“控件”可触发,它似乎是只要 onboarding 时用,所以没有可拦截的东西。

Profile 图片的契约WAS已经捕获到手(它是 JSON,不是 multipart),但它上还没有构建任何工具:要在浏览器内还原一样需要WebP 的body(且 width <=800),而这个包没有这个依赖。

会话 token

工具

effect

instahyre_login

Email + 密码,走明文 HTTP。没有浏览器。

instahyre_login_bowser

打开一个窗口做 Chrome 登录。三个能够拉起浏览器的工具之一。

instahyre_auth_status

向 server 询问 session 是否还活着。它可以诚实返回 false。

instahyre_session_info

凭据是什么、什么时候浪效、session 了何时最终失效、以及如何续期——包括 renew 时会启动一次 headless 浏览器和可能代价。verify_live=False 时则没有任何的次数和启动任何 browser。

instahyre_reauth

从浏览器 里静默 renew——headless、没有密码、没有窗口的,而且它不跳登录。当一个工具返回 auth_required 时,先用它。它若不行当每次,会告诉你七种之一 是哪一种,并且把上一个 session 字节逐字节恢复。

instahyre_logout

从本地擦删除已保存的 cookies。不会留下浏览器 profile 不用。所以 instayhre_reauth 常常就可以够直接回回去登录。

这个平台没有的能力

去查找之前值得先知道。下面每一项都是被重量验证了的“不存在”,而(不是“没有实现”。并且 instahyre_server_info 会在运行时把它们再报一遍。

  • 薪资:无。 在抽样的 2,235 条记录中,没有任何一条含有薪酬字段;在抽样的 45 个职位描述中也没有任何一条包含薪酬数字(已用阳性对照验证)。这个 API 上根本不存在任何薪酬渠道。

  • 发布日期:无。 任何端点上都没有 posted_at/created_at。职位 id 是顺序递增的,因此 instahyre_sync_index 在本地写入若干个 first_seen 将是永远唯一存在的新鲜度信号。

  • 排序:形同虚设。 API 接受一个 sort 参数,但可以证实它会忽略它——sort=relevance、date、- id 返回的首页完全相同。此处的任何工具都不提供 sort 参数;instahyre_rank_jobs 只是在本地排序。

  • 申请人数、公司评分:无。 不存在任何竞争信号。

  • 混合办公 vs 驻场办公:未建模。 Work From Home 是唯一的远程办公关键词(约占语料中的 8.6%);其余办公形式的设定根本不在数据里。

  • 已保存或已收藏的职位:无。 没有收藏职位功能。收藏搜索是存在的(/ours_ob_searches),但职位端没有任何对应的等价功能。

  • **消息正文:不可获取。错误取消。 此条记录是错的,保留它作为本次修正。 原文这样写:“消息需要一个 conv_id,并且所有端点都无法列举会话,所以会话只能到网站上阅读。”实际这个端点存在的——/ours_ou_ersation/candidate_conversation——而且普通 httpx 即可访问。最初的搜索在 /resume_ob_emails/ 中寻找消息资源旁边是否有一个会话资源,结果发现两个 404,并从这两个 404 做了泛化。见 instahre_list_conversations。真正值得吸取的教训是:“没有哪个端点能做 X” 这种话,其实是在说“你在哪些地方找过”;本文件在说一个平台不能做某事之前,都应该先说明曾搜索过哪些命名空间。

  • 招聘者活动的机器时间戳:无。 action_date 到达时已经是人可读的格式化字符串——"13 hours ago"、"Aug 12 at 3:47 PM"。读它就好,不要对它做计算。

  • 单个机会的详情路由:无。 candidate_matching/<id> 返回 HTTP 400,不是 HTTP 404。instahre_get_opportunity 是通过横扫队列来找到一条记录,所以它整体上是在 composition 而不是 fetch。

  • 招聘者联系方式:无。 无论 agency 活动还是活动记录都只提到招聘者及其公司;整个候选 API 上没有任何邮箱或电话。

这个客户端已经替你处理掉的陷阱

每一条都经过线上实测,而且每一条如果不处理,都会成为一次沉默的错误答案。

  • company_size 代码不是逐一对应的。 1 表示小,2 表示圆形,3 表示。有精确的分区算术证明:4022 + 2286 + 7147 = 13455=未筛选的总数。如果你直接默认 1/2/3 = 小/中/大,会把所有 中型/大型的公司都标错。工具只取字面词,绝不取代码。

  • 地点区分大小写。 Bangalore 有 7,000+ 条职位;bangalore 直接 HTTP 400 “Invalid location”。每个 location 都要经过一个解析器,它负责把大小写改对最近的相似项,并给提示。

  • limit 不仅有上限,也有下限。 limit=1 会返回 35 个对象。没有“只要数量”的轻量调用;必须读取 meta.limit。

  • 技能字段会静默失败。 地点、公司、行业、规模、工作年限全都在服务端校验,如果填错就会 400;但 skills 不校验——无法识别技能会返回 HTTP 200 和零结果。这和真的是个无聊结果的市场完全无法区别。当搜索返回空时,结果中带有 diagnosis,指出哪个技能没有匹配到任何记录。

  • 一个缺失的 job id 会返回一个 404 的 48 KB HTML 页面。 不会解析它;它会抛出一个带类型的 not_found。

  • 同一个职位会出现在多个批次 id 之下。 一个 841 职位的样本中,有 41 个 (company, title) 对出现在多个 id 之下,其中一个重复了 7 次。结果按 id 去重,并标注 duplicate_ids。

  • 每个搜索结果都声称有 candidate_opportunity_employer/:id,但它返回 404。 这是一个死引用;忽略它。

已认证层级上的陷阱

  • status 在队列上会被接受,但被忽略。 它看起来就是最显然的筛选参数。status=1 和 status=2 都返回了完整未筛选的 228 条。真正有效的是 interest_facet,而且这也是这个客户端唯一会发送的筛选字段。一个说谎的筛选器比一个报错的筛选器更能坑人。

  • 队列的筛选拼写和搜索接口不同。 队列这边是单形的 location 和 industry_type;搜索接口 job_search 则是复数形式 jobLocations 和 industry_types。如果你把搜索端那套拼写传进去,实际上不会过滤任何结果,而且看起来像很宽的结果页面,也正因为如此,这两个 builder 故意不共用。

  • 队列裸请求是 HTTP 400 + 空 body 的——没有字段名、没有任何提示。它必须带显式的 limit。所以每个队列调用都会给它带一个。

  • 两个队列资源的数据不一致。 candidate_matching 返回 228,candidate_opportunity 返回 238(严格超集),fetch_filter_counts 也是 238。默认值是网站本身显示的数值;传入 include_unindexed=True 才能取到更宽的那个。

  • 先翻页再排序,结果会骗人。 服务器是按照自己的顺序返回队列的,所以如果你对翻出的一页再排序,并把排在最前的叫“最佳匹配”,这其实就是“随机一页里的最佳”。实测:limit=5 时,把一个 4.15 浮了上来,而同一个队列里后面另一个 16.05 则沉在下面。instahre_list_opportunities 会先把整条队列都拉回来、再排序、再分页 —— 无论如何只发一次请求,结果就一致、真实。

  • is_strong_match 不能做筛选条件,而且它会说得很响亮:{"error": "The 'is_strong_match' field does not allow filtering."}

  • has_valid_number 并不是 number_verified_at。 前者表示这个号码格式对了,后者表示真的做过 OTP 验证。同一个账号上这些的情况可能不一样,所以工具给它们起了两个不同的名字(phone_format_valid vs phone_)`),这样就不会有两个工具看起来互相矛盾。

  • 活动流的 employer 字段是中间招聘公司,不是用人的公司。 它通常是人才派遣公司。job.hiring_comp_name 才是这次职位最终的雇主。如果把两个合并/当成同一个,每一件事件都会被存错,所以两个都保留。

候选人的 id——不需要浏览器也能找得到

所有 profile 和 settings 路由都是“只有详情接口”——对集合做 GET 会返 405,所以在没有那个数字候选 id 之前,它们全都不能用。而这个 id 只会由服务端注入到已登录 HTML 页里,普通客户端恰好无法读它:这类 HTML 链路被 Cloudflare 锁定(对 httpx 是 403,对 headless Chromium 是一张 "Just a moment..." 中间页),而 /api/v1/* 是放行的。

这看起像必须用浏览器走数据路径了。其实不用。/andidate_misc/profile/email(不是)既是集合,又能响应 GET,并且每一行都带有其主人的 resource_uri。一次便宜请求就能得到这 id;然后它会缓存 30 天。如果一个候选人的某个人没有教育经历,instahre_get_profile 会抛出 candidate_id_unavailable,并告诉你如何修复——而非返回一份空 profile 让你误以为“什么还没填”。

端点图本身也是诚实恢复出来的:把浏览器指向签名页面一次,在 router 上打断所有非 GET 请求,然后记录下它发起的 XHRs。这些路径全部按站点自身应用发出的形式唯一记录——所以它们都不会带结尾的斜杠。

Webhook agency 过滤器

Instahyre 约有 84% 的职位发布来自第三方招聘外包机构,不是用人公司。这个筛选标志是免费而且精确的——一条职位详情要么有 agency_unction_names,要么有 job_function_names,绝不会两个,而这一 key 的选择在 45 个样本记录里和 recruiter_company_name != hiri_company_name 全部一致。

它的难点在于:字段挂在详情对象上,而不是搜索结果上。所以 include_unfoundraunfiltered 里当 exclude_agencies=True 时,每页的每个职位最多都会多花一次请求。结果是即时缓存 6 小时,instahre_sync_index 会预热相关结果。

安全

Instahyre 的申请无限不能被撤回。 它的 FAQ 里会写:申请是系统自动发出的;没有撤销按钮,没有后续支持通道,而且雇主会立刻看到。下面所有内容都是从这一个事实出发的。

  • Apply 是单次性操作,而且不可逆。 instahyre_apply 恰好接受一个 opportunity_id_。没有批量申请工具,将来也不会有——Instahyre 的 API 有 apply_bulk/,而暴露它会让单次调用在整个队列上都不可逆。被禁用的路径已固定在 constants。FORBIDDEN_ENDPOINTS 中,并且有一个测试会遍历这个包的 AST,以证明没有任何调用点能构造出这些路径。

    现在两个批量 URL 都被屏蔽,而不是只屏蔽一个。 Instahyre 的每个机会端点都有 ES 和旧版(legacy)两种变体,而之前的禁用列表只包含旧版拼写——当前账号解析到的却是 ES。被屏蔽的是永远到不了得那个路径,而真正能到达的路径反而没有被屏蔽。

  • 申请请求本身也是错的,而且从未真正发出过任何申请。 这个请求体转写自 Instahyre 自带的前端 dispatchser,从来没有真正执行过。独立重读上述 dispatcher 后发现了两个错误:

  1. enableCandidateESOpps 切换的是 $resource 服务,而不仅仅是请求体——所以 URL 和请求体中的 id 键是一起变的。旧代码把 ES 请求体(job_id)和旧版 URL(candidate_opportunity/apply/)配在一起,而前端从不产生这种组合。

  2. is_activity_page_jab 在 每一次调用上都会设置,两个分支都是,而旧代码里它完全没有出现。

实际生效的分支通过三种独立方式确证:dispatcher 源码、从实时页面上读到的 enableCandidateESOpps 标志,以及页面自身 XHR 所用的服务。instahyre_verify_apply_target 会重新测量它,并在不一致时报告 MISMATCH,而不是默默切换。

这就是“不通过实际执行去测试一个不可逆操作”的理由:这个契约错误了数周,但发现它没有成本为零,因为从来没有发送过任何请求。

  • Instahyre 自己的 UI 在申请时没有确认对话框。 那条流程里每个弹窗都是在 POST 接受 之后才出现的。这里的 confirm=UmTrue 闸门比网站本身的更加限制。

  • 收件箱的 READ 层不能修改数据,而写入层只可以回复。 有四个收件箱端点会修改数据——send、star、toggle-read 和 mark_all_read——而每一次读都会先用子串匹配检查全部这四个,包括 send_message。send 是经由另一个单独 ALLOWLIST 来触发的,里面恰有一个路径;这跟 allowlist 中“一个洞”是两码事:任何不等于那一个值的内容都会被拒,包括没人想到要列出的一类行为。mark_all_read 正是两套守卫都以 path 为键而永不看动词的原因——它是一个 GET 请求,会呼叫批量清除未读状态,而且与列表端点共享前缀,所以一个普通的“遍历资源、看看下面有什么”的探测就会在无请求体、无任何警告的情况下清掉你的未读标志。

  • 回复无法撤回,而且这个工具正是围绕这一点构建的。 Instahyre 有没有 unsend、没有编辑、没有删除。confirm=False 不发送任何内容,只返回服务端报告的接收者、线程的公司名与角色、输入的消息原文以及请求体的确切字节。空消息会被拒绝(Instahyre 自己 compose 表单根本不做任何验证——这里每一个护栏都是我们自己加的)、附件永远不会发送,因为从未测量过它们的元素形状;发送完成后会重新读线程,以确认消息到底到了。如果确认失败,结果会提示 不要重试:因为一个已投递的消息就算重试重复发送一遍,也无法撤销。

  • Profile 写入是先快照、后验证。 快照会 在 请求发出去之前就落到磁盘,所以即使进程在写入中途崩溃,恢复点也能保留下来。收到 200 永远不会被视为成功——每次写入都会重读并比对;如果对不上,就报告 verified: false 并附上差异。

  • 技能写入会把每一行幸存下来的技能逐个字节回显给你。 该技能资源是一个 整体替换集合(full replacement set)——我们通过添加一个 canary 技能,再发送一个不包含它的 payload 来测量出这一点,而它的确把它删掉了。因此,任何不完整的列表都等于删除了删除指令;而回显正是把 payload 隐式“这些就是全部”的主张变为真的地方。对该资源执行 DELETE 会返回 405, Allow: GET,PATCH;在知道这一情况之前,一个逐行删除行删除的恢复路径就被移除掉了。

  • 移除同样是同一个机制,而且是有意为之。 instahyre_update_skills 除了 add= 外还接受 remove=,两者都在同一个 PATCH 中完成——所谓移除,就是不把它复制进 payload。类似是因为平台限制该列表上限为 20,而该账号已达上限,所以每次追加都等于一次交换;instahyre_skill_gap 的 dead_weight_skills 能力会把出现在 零 个匹配岗位中的技能名都列出来,而需求高的技能却排在该列表之外。限制全部写在代码里:名称必须精确、且不区分大小写匹配(绝不按子串匹配,所以移除 “System Design” 不会连带 “System Design Patterns”);“不在 profile 上的名字会报出来,而不会被默默忽略;一次调用中同时添加和移除同一个技能会被拒绝;清空列表会被拒绝——在一个反方向的人才市场上,没有技能的 profile 不是“简短”的 profile,而是“找不到”的 profile。服务器忽略的删除技能会被报告为 removal_did_not_take,而不是算成功。恢复已删除的技能,只是把 NAME 拿回来、但会获得新 id:它原来的那一行在服务端已经没了,所以会用“新技能”的形状重新发一次,而不是赌服务器会习惯于一个死 id。

  • restore_profile 会验证自己的参数。 snapshot_id 来自 agent 的一个可调用工具,因此它是不可信输入,命名的是一个文件。以前用一个 "../not-a-repeatns" 去探过,结果读到了快照目录之外的某个文件,文件里没有任何技能;于是把全部四个技能都删了。现在它要求 id 必须匹配 [0-9]+-[a-z0-9-]+,解析范围固定在该快照目录中,并拒绝任何含零个技能的快照;因为从空快照恢复不是“什么都不做”,而是“删除所有东西”的指令。

  • 拒绝同样一去不回。 instahyre_decine_opportunity 是同一个端点,只是翻转一个布尔值,它是永久性的,而且会反馈给 Instahyre 的匹配算算法。同样的闸门,同样的警告。

  • **confirm=False 是默认值,什么也不发送。**它只会把将要发出去的精确请求原样返回——method、URL、body、headers——外加 role、工作地点和匹配分数,好让人在事情发生之前先看清楚。

在 confirm=True 和 一个 POST 之间,有四道守卫,而且每一道都真的可能失败:确认本身;对于同一 opportunity 上重复做同一次不可逆操作的拒绝;实时检测 target path 是否在禁用列表中;会话没有 CSRF token 时,拒绝未签名放宽发送。每道守卫都有一个测试,这个测试在守卫被移除时会被证明会失败。

  • 请求的长相设定从来不是通过真实发送请求去得到的。 它是从 Instahyre 自己带的一段前端 dispatcher 中转写过来的。这种形状的请求,这个包从未执行过;这一点已经写在工具 docstring 和每次 preview 里。

  • Profile 写入故意只给预览。 只验证读的形态,不验证写契约。验证写契约意味着真的去写线上 profile——而它会产生未来的每一个匹配。一个字段形状稍有小错误的 PATCH 可能把某个字段清空,或者返回 200 却什么都没变;而“静默不做事”正是本服务存在的目的不出现捕捉失败。这是因为 instahyre_preview_profile_udate 会把 request 显示出来,典型的就停了。

  • 确认的闸门对调用者来说是建议性质的,而不是结构性的。 这里没有任何方式能知道“人类是否真的看了 preview”;从“什么也不做”到“一个永久动作”之间只有、一个布尔值的距离。沿途每一处 docsting 都告诉你先 preview 再 confirm。

  • 一个请求之间的请求约 相隔 经 1.2 秒并加抖动;重试用指数退避并且遵循 Retry-After。只处理个人本人的请求规模。

  • 密码只会在整个请求中使用一次,从到日志、缓存或折片。Instahyre 会在 settings payload 中把 password 字段回传——因此这些字段在任何返回、缓存、记录之前就会被 剥掉。 并且有一个测试用仍带了这些字段的 fixture 文件证明了这一点。_State/(会话 cookie、索引、浏览器 profile)都被 gitignore;已提交的 fixture 也经过脱敏处理,不会有个人数据。

并发状态

  • _State/ 位于包旁边或目录 $INSTAHYRE_HOME:

  • instahyre.db —— 双速率缓存、职位索引、以及 corpus 跟踪器(first_seen/last_seen)。

  • session.json —— cookie jar。永远不包含密码。

  • browser_profile/ —— 持久化的 Chromium 浏览器 profile,只负责 Google 登录。仅在 Google 登录。

评分

instahyre_rank_jobs 和 insta_inbound_sidegest 都使用同一个共享 jobcore 引擎来打分,Naukri 和 Uplers 服务也用同一引擎;所以三个招聘板上的匹配分,含义相同。这是它是个硬依赖:没有本地备选打分器,缺 jobcore 时会出现 ImportError,错误信息明确会显示修招方法,而不是悄悄给不同的数字。

曾经有过一个 fallback,藏在 ../core/src 的 sys.path注入里。后来就消失了,而且是有意为之。fallback 里完全没有做技能 apasing,所以岗位上的"Node.js"永远配不上 profile 上的"nodejs";它在同一个 fit_score键下产出的每个分数系统性偏低;它又自带一套0.6/0.4的切分和全自的 verdict band,早已偏离jobcore;而权重变成可配置之后,它就成为第二个 *configurable 不了* 的引擎,盖住真正可配置的那个。sys.path 注入本身有自己的问题:它能 import 正常但什么也不安装,所以没有版本被锁定,importlib.metadata` 甚至看不到它。

策略:jobhunt.json

数字是值,不是字面量。 权重、verdict band、bonus、经验曲线和词汇量,都放在所有三台服务共同读取的中 jobhunt.json;你改它,下一次调用就会按新值打分,哪都不用重启。这不要需要配置读出的返回符合策略的、是你打的一份,一手指。instahyre_config() 则报告出当前生效的策略、它的两个 fingerprint,以及从哪个文件加载的——如果不存在,就列出所有试过的路径。。source: null 即内置默认值,是“发布时行为”。

两个分数只有当它们的 scoring_hash 相同时才是可作直接比较的,所以只要策略不再是默认状态,每个评分回都要带 scoring_hash。该 hash 覆盖计算部分本身——权重、bonus、cap、band。policy_hash 则更广,覆盖全部评分输出和候选项——是 config 核读出来的那个标识符答案不能代表它,因为候选项是“callat”带来的的一部分,不是结果本身的属性。hash 读两个都会读出来;而这“一对”才能把一个原始调度映射回产生它的 config。

给 stahyre_rank_jobs 或 insta_inbound_side 传 explain=True,就可以在单独一行上给出每个分数后面的运行过程——权重、技能分数/经验基础分配、每一项 bonus和 cap,以及用到的 scoring_hash。它不多消耗任何额外请求,而且默认关闭,因为输出那块比被解释的那个列本身要大出几倍。

这份文件无法让这个服务拥有任何自主性。 在源码里,insta_apply 和 insta_decine_opport 每行要求人、confirm=T যে 的确来自人类,得到的;这里没有任何 “agent 权限”也没有 scheduler;以及 jobcorecore全部拒绝从外部文件加载它的 C 类键(agent enablement、agent mode、apply threshold),会在 tier_c_efusals 中逐条写有哪些是被拒而不是默默忽略。

scripts/clean_install_check.py 会在“给出的安装无法 unit 解决 jobcore` 时直接失败。

测试

576 个测试,完全离线——每次 HTTP 调用都经由 httpx.ockTransport 走预先从真实 API 抓取的 golden fixtures(黄金样例);没有被 mock 的路径会大声失败,而不是返回空结果。写路径只以 mock 形式被演练;没有任何测试向真实应用发出过实际请求。

tests/test_inbound_safety.py 守卫的是不可逆那一半。它断言"没有 POST",而不仅仅是"有异常"——只检查是否 raise 的测试,在一个"先发送、后抛错"的实现上照样会通过。它还遍历了包的 AST 来枚举写路径面:每一处 .post( 都用裸常量命名端点,而 .patch( 恰好只出现在一个模块中。

tests/test_scoring_policy.py 封住了配置接缝:从改动前 scorer 留下来的 15 条 golden 用例证明默认值没有动;其余的用例则证明 jobhunt.json 中的一个权重确实能使其变动。它曾在一种故意放水的构建上跑过——这种构建把策略接住后抛掉,而那正是这个测试存在所要抓住的 bug——那组运行时有 6 条断言变红,而 parity 对照用例依然保持全绿:

$env:PYTHONPATH="scripts"
venv/Scripts/python -m pytest tests/test_scoring_policy.py -p permissive_scorer_control
# 6 failed, 40 passed

scripts/permissive_scorer_control.py 附带了那个插件,并说明哪六条会失败,以及另外四十条为什么应当该能在这里活下来。

tests/test_hardening.py 钉住了三个当时带着全绿上线、之后才是通过 mutation 才被发现的缺陷,一个能清空 profile 的 restore 操作、一个永远数不到上一位的 withheld 消息计数,以及两个从匹配分类体系里避走的分页参数。该文件中的每条测试都已把缺陷重新战术放回去重跑过一遍,确认它真的会从绿变红;一个从没人证过它能失败,那就算不上measurement,只是意断言(claim 而已)。

venv/Scripts/python -m pytest tests/ -q

检查全新安装

venv/Scripts/python scripts/clean_install_check.py

把已提交的代码树放进 work 一次性工作区,building 一个全新 venv,把上面"安装"章节从零新做一遍,import 服务器且跑一气测试套件,然后去掉工作区。你的工作树与你的 venv 从头到尾不被碰。

在动 requirements.txt 之前 / 或 pyproject.toml 之后,在相信本地那套绿之前,请做一次。本地 venv 不过是过去某次解析结果的缓存,它无法让看今天的你实际解析出来会得什么。2026-08-20,本家的兄弟服务器 naukri 应当还没有过的无边界声明了一个的总限 mcp[cli]>=1.25.0;但随后 mcp 2.0.0 把 mcp/server/astmcp 移到了 mcp/server/mcpserver,一次干净的 resolve 顺带选上了它,结果 naukri 的 55 个测试模块在 collection 上下丢了——`"5 deselected, 55 errors"*,跑起 0 个例——而每组本机的 run 都仍旧为绿色,因为它们 venv 装着的是 2.0.0 发布前就已经在的 mcp 1.26.0.

这台服务器不会被波及到那次的定点移植,而且这个"您是测序出来的",不是"下结论":因为它直接用独立的 fastmcp 工程,并在 import instahyre_server.server 之后,实际装入的模块是 mcp.server.audit 及其 mcp.server.models,却没有 mcp.server.fastmcp。另外 fastmcp 还把自己的最后一个依赖版本锁在 mcp<2.0。它共享的确实是那种病根:出于它 framework 的统一包,给写了无界的 >=。所以目前的 fastmcp 同样被 cap 在"下一个未测过的 major"(<4,而整套也已有 3.4.7 实测绿),tests/test_requires_ps.py 则通过对 requirements.txt、pyproject.toml 进行我们读为文案来维护这句铁链。但现在如果写成这个将已安装版本为点,那就,那条语句于同一个腻号的 venv 里就能"轻轻地 pass 过去",而这个 venv 正打包着这个 bug。

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    C
    maintenance
    Enables MCP-compatible agents to search live job postings, score resumes against specific postings, and create or update tracked applications through their pipeline.
    4
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables recruiters and hiring operators to work inside the Greenhouse applicant tracking system from MCP clients, with per-user permission-scoped reads and a preview/apply flow for safe, auditable writes.
    3 npm
    MIT
  • F
    license
    Not graded
    quality
    A
    maintenance
    Enables AI assistants to search jobs, retrieve job details, and view or update career-profile data such as experiences, job summaries, career goals, and technical skills through MCP tools.
    -