Instahyre MCP
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 的问题;它并没有成为数据路径的一部分。
工具 | 使用浏览器? | 为什么 |
| 是,可见窗口 | Google OAuth 是一段任何 HTTP 客户端都不能完成的跳转、洪舞。 |
| 是,可见窗口 | 读取一个服务端注入的页面标志,它决定应用会把申请提交到哪个端点。没有 API 暴露它,而该页面受 Cloudflare 门户控。申请不可撤回,因此在这里用一个浏览器而不靠猜测,是值得的。 |
| 是,无头模式 | 重新收割该持久资料自身、长期有效的 |
其他 44 个工具 | 否 | 纯 |
这两个 可见窗口 的浏览器工具会在路由处拒绝每一个非 GET 请求,除了 Cloudflare 自己的 /cdn-cgi/ 挑战握手——它不会改动账户任何内容,没有它,验证就永远无法完成。二者都不会点击任何东西。
这里与姊妹端 Naukari 服务器之间还是保留了有意的差异;Naukari 需要一个持久 Chrome 配置文件、CDP 桥,以及一千四百行左右的 data path 反 bot 管道。而这个项目里,浏览器是偶发的、选进的。
我们不装浏览器 User-Agent。 它什么都不买(API 接受这个诚实的标识),而且 Instahyre 的条款也不许伪造 headers。若哪天 API 的这个豁免被移除,客户端就会抛出一个 ChallengeDetected 类型错误——这是让你停下来并重新评估的信号,而不是绕过它的信号。
Related MCP server: MCP Resume & Email Assistant
安装
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/ -qThe 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工具
做有联系的工作。
公开 —— 无需登录
工具 | 做什么 | 请求数 |
| 搜索实时职位。可用筛选项:技能、职位类型、地点、公司、行业、规模、经历、职位。 | 1 |
| 单个职位唯一的描述、经验带、招聘者、机构判定。 | 1(缓存 6h) |
| 雇主 profile 加上他们当前所有活动职位。同时是一个“公司是否在平台”的可 oracle。 | 1 |
| 给定的区块的刻画市場总览,且在这份数据全无职位记录。 | 1 |
| 搜索,然后照与你自身技能匹配度排列成页。技能来自 | 1+(使用 profile 回退时 +2) |
| 将一份数据分页分页导入本地索引,并报告从上次以来哪些是 NEW。 | 1/page |
| 58 个职位职能及其 id。 | 1(缓存 30d) |
| 合法的位置 token,并已分组。 | 1(缓存 30d) |
| 74 个行业的类型和 id。 | 1(缓存 30d) |
| 缓存状态、请求数,以及这个平台无法提供的内容。 | 0 |
| 现生效的评分策略、其哈希,以及它来自哪个文件。 | 0 |
已认证 —— 入站分类
The half that matters is the one that matters。
工具 | 说明 | 请求 |
| 从这里开始读。 队列徽章、谁看过你的简历、未读消息、得分最高但尚未被触及的匹配,以及上次运行以来新出现的内容。 | ~5 |
| 被策展的队列:雇主为你匹配的职位,带真实匹配分数。在全队列中统一排序。 | 1 |
| 单个匹配,由队列记录 + 公开职位详情 + 该雇主的同类职位组合而成。 | 2-3 |
| 队列的切面:状态、地点、行业、雇主、规模。不含记录。 | 1 |
| 谁查看过 / 联系过 / 没有把你列入候选名单。这是这里最易消逝的信号。 | 2 |
| 每一份申请和婉拒,附状态。 | 2 |
| 你的个人资料,加上按每一项缺口所costs的代价排定的完整性缺口。 | 1-2 |
| 可见性、通知、屏蔽的雇主。 | 1-2 |
| 申请一个机会。不可逆。 除非 | 0-1 |
| 将一个标记为“不考虑”。同样不可逆,同一样门阚。 | 0-1 |
收件箱——读取,以及恰好一个写入
招聘者的对话和消息正文。每个 READ 在发出前都会对照一个“会变更”的路径片段清单来做检查,所以读取层不能发送、标星或标记已读——send_message 仍然在那个清单上,读取端也仍然拒绝它。
回复走另一扇门:一个 allowlist,里面只有恰好一个 URL。部分发消息、标星、已读、批量全读,不仅被拒绝,它们连构造它们的分支都根本不存在。
工具 | 说明 | 请求 |
| 会话列表,附带从职位合并进来的公司和职位。过滤器:状态、未读、标的星、任意文本。 | 1(若 |
| 单个会话中的每条消息,以文本、最早的置前。一个不属于他的 | 若线程返回为空则 +1 |
| 未读 / 已标星 / 已标星且未读的数量合计。 | 1 |
| 往一个会话发送一条回复。无法逆转——必须 | 预览 2-3,真正发送 4-5 |
一个诚实的中文提示,写在工具自己的 docstring 里: „读取“一个会话”可能在 Instahyre 的那一边把它标记为“已读”。 该网不发送“标记为”已读的消息——ii 只是在本地减掉 badge 计数——但这种只有当 server 在取回消息时就真的做了标记,才自洽。列会话和计数值是可以证明不会产生修改的;但取一个会话并不能被证明没有产生修改。这项没有被测试,因为收件箱当前没有会话。
写入 —— 这些会更改你的账号
工具 | 作用 | 请求 |
| 增加技能。只增,先快照,然后验证,除非 | 2-4 |
| 设置职位、公司或经验年限。同一个门限。 | 2-4 |
| 把技能列表放回到某个快照。 | 2-3 |
| 磁盘上的恢复点。 | 0 |
| 打开浏览器。 重新测量一次申请实际上会 POST 到的终结点。 | 0(浏览器) |
捕获写入 —— 建设之前就测量过
该层中的每条请求都是在代码写出来之前录成的。另有五个写入面在 2026-08-手工合成并被当场拒绝——because 其中每一个都没有已经录下的 request body;而且用猜的 body 发起写入,反而不如不加任何工具。猜错的通常就无害地 400 一下;而猜对了一半的,则会成功,且做一个没人选的操作,这种事在这个平台上为永久的。
scripts/capture_rite_contracts.py 就是解除这种方式。它打开真实已登录的浏览器,在路由层上中止所有非 GET 请求,并记录被中止了什么——方法、URL、body、headers。它只会在 s 从页面内部发起一个 POST 并看到 路由去阻断它之后才会继续走,所以“没有发出任何请求”就是一项实际测量,而不是假设。
工具 | 做了什么 | 契约测量值 |
| 提交其工单。有人来读,无法删改,除非 | Wire —— 记录并在路由层中断 |
| 打开/关闭某个已存搜索结果对的提醒。可逆。根据标志位把查询词串也发送了,像网站原样。 | 已发布源码中的整个函数 |
| 要自己的内推链接。不联系任何人。 | 已发布的源码 |
| 复查 Instح 会把哪些人当作可邀请列表。一个读取 — 在其客户端也是 GET。 | 已发布源码 |
| 邀请某人。不可逆。 预览会列出每个要求收件列表的人。 | 已发布的源码 |
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 |
| Email + 密码,走明文 HTTP。没有浏览器。 |
| 打开一个窗口做 Chrome 登录。三个能够拉起浏览器的工具之一。 |
| 向 server 询问 session 是否还活着。它可以诚实返回 |
| 凭据是什么、什么时候浪效、session 了何时最终失效、以及如何续期——包括 renew 时会启动一次 headless 浏览器和可能代价。 |
| 从浏览器 里静默 renew——headless、没有密码、没有窗口的,而且它不跳登录。当一个工具返回 |
| 从本地擦删除已保存的 cookies。不会留下浏览器 profile 不用。所以 |
这个平台没有的能力
去查找之前值得先知道。下面每一项都是被重量验证了的“不存在”,而(不是“没有实现”。并且 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_validvsphone_)`),这样就不会有两个工具看起来互相矛盾。活动流的
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 后发现了两个错误:
enableCandidateESOpps切换的是$resource服务,而不仅仅是请求体——所以 URL 和请求体中的 id 键是一起变的。旧代码把 ES 请求体(job_id)和旧版 URL(candidate_opportunity/apply/)配在一起,而前端从不产生这种组合。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 passedscripts/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。
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseBqualityDmaintenanceProduction-ready MCP server for Greenhouse ATS with 175 tools for recruiting teams — manage candidates, applications, jobs, interviews, and hiring pipelines. Role-based profiles (full/recruiter/read-only), composite workflow tools for pipeline views, analytics, candidate search, and bulk operations.1005MIT
- FlicenseAqualityDmaintenanceEnables resume parsing, querying, and email notifications via MCP tools.31
- AlicenseNot gradedqualityDmaintenanceEnables querying and managing Greenhouse recruiting data through MCP, including applications, candidates, jobs, and rejection operations.MIT
- FlicenseNot gradedqualityCmaintenanceExposes job-posting scanner tools (search, company health, fit scoring, dry-run outreach) via MCP, enabling offline job search and evaluation from any MCP host.
Related MCP Connectors
GetJobzi MCP server for job search, application tracking, and career forecasting.
Managed LinkedIn MCP server for AI agents: search, connect, message and enrich on accounts you own.
AI job search MCP — fact-checked jobs, application tracker, alerts. ChatGPT, Claude, Cursor.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/Sundeepg98/instahyre-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server