mcpField
W
Watchtower

Watchtower

远程服务
watchtower.lat商业与营销01 周前更新

关于 Watchtower

Watchtower 为 AI Agent 监控科技公司和初创企业的职位发布,并以结构化数据提供符合条件的新职位。它支持自然语言条件、定期检查、变更查询和 webhook 通知,也可搜索当前职位。

主要功能

  1. 01按自然语言创建职位监控
  2. 02覆盖目录中的公司职位板
  3. 03筛选地点、资历、薪资和经验
  4. 04查询新增、删除和更新事件
  5. 05通过 webhook 推送变更
  6. 06搜索当前职位并分页查看

适用场景

  1. 01等待符合薪资和经验条件的新职位
  2. 02监控指定公司的招聘页面
  3. 03让 Agent 收到新职位时继续处理

MCP 相关

提供 Streamable HTTP MCP 服务,公开职位监控、搜索、变更读取及监控管理等工具;同一服务也支持 REST。

配置

远程服务无需本地安装。选择你使用的客户端,复制配置或使用一键安装。

服务地址https://watchtower.lat/mcp?ref=registry
一键复制安装命令
Claude CodeTerminal
Cursor.cursor/mcp.json
VS Code.vscode/mcp.json
Claude Desktopclaude_desktop_config.json
Windsurf~/.codeium/windsurf/mcp_config.json

命令在终端中运行;JSON 粘贴到对应客户端的 MCP 配置后重启。需要授权的服务会在首次连接时提示登录。

工具

8
参数类型必填说明
urlstring否可选。将监控范围限定为一个招聘板或招聘页面,例如 https://boards.greenhouse.io/acme、https://jobs.lever.co/acme、https://acme.wd5.myworkdayjobs.com/Careers。不填写则监控所有受监控的招聘板。
urlsarray否可选。使用相同筛选条件监控多个招聘板(最多 25 个)。返回监控项和每个 URL 对应的错误。
labelstring否写给自己的一条简短备注,说明你为什么要监控此项。
querystring否用通俗语言描述要监控的条件:职位、地点、薪酬、经验、级别、远程办公。例如:"iOS jobs in Austin making at least 150k a year with a maximum of 6 years of experience"、"senior backend roles in New York or remote paying $180k+"。请在响应中检查 interpreted。
keywordsarray否仅报告 title/location/department/company 中包含其中任一词语(作为完整单词)的职位,例如 ["iOS", "Swift"]。
locationsarray否仅报告 location 中包含其中任一项的职位,例如 ["Austin"]、["Berlin", "Remote"]。
seniorityarray否仅报告这些级别的职位,级别根据 title 推断。"mid" 表示 title 未标明级别。
min_salarynumber否职位必须能够达到的年薪,例如 150000。与招聘信息所列薪酬范围的上限比较(时薪和月薪会换算)。
remote_onlyboolean否仅报告 title 或 location 标明为远程办公(而非混合办公/现场办公)的职位。
webhook_urlstring否可选的公开 https URL,用于在检测到符合条件的变更时接收带签名的 POST。无论是否设置,轮询 get_changes 都会继续有效。
all_keywordsarray否仅报告包含以下所有词语的职位,例如 ["data", "scientist"]。
client_tokenstring否仅当此对话在应用连接前已使用 Watchtower 令牌(wt_...)时才填写。否则请留空;切勿向用户索要。
include_unknownboolean否许多招聘信息未注明薪酬或工作年限。true(默认值)仍会报告这些职位,但不包含 salary / experience_years 字段;false 则仅报告注明了符合条件数值的招聘信息。
salary_currencystring否min_salary 的 ISO 货币代码,例如 "USD"。若职位薪酬以其他货币标明,则不予报告。
exclude_keywordsarray否若职位提及以下任一词语,则永不报告,例如 ["manager", "clearance"]。
interval_minutesinteger否检查频率,以分钟为单位(最短 5 分钟,默认 60 分钟)。与其他监视器共享的资源采用最短间隔。
max_experience_yearsnumber否职位要求的最高工作年限,例如 6 年。

详细介绍

Watchtower

面向 AI Agent 的技术岗位监控。只需描述一次你想找什么,就能以结构化 JSON 获取技术公司和初创公司的招聘网站上最新发布且符合条件的职位。

Agent 不擅长等待职位发布。会话只持续几分钟,重复查看招聘页面又很费劲,而“奥斯汀有公司发布 iOS 职位了吗?”也会变成每天重复运行同一个搜索。借助 Watchtower,Agent 只需用自然语言创建一次监控:

{ "query": "iOS jobs in Austin making at least 150k a year with a maximum of 6 years of experience" }

Watchtower 会按计划检查技术公司和初创公司的招聘网站,并记住哪些职位曾经开放。之后,get_changes 只会返回符合条件的新职位;有新职位出现时,webhook 还可以唤醒 Agent。Agent 也可以通过 URL 监控某一家公司的招聘网站,并获取 JOB_ADDED、JOB_REMOVED 和 JOB_UPDATED 事件。

  • 无需 URL。 通过 query 创建的监控会覆盖 Watchtower 监控的所有招聘网站:内置的技术公司和初创公司招聘网站目录,以及任何人通过 URL 监控的所有网站。
  • 薪资和经验要求。 招聘信息中注明时,职位会包含 salary 和 experience_years;监控可通过 min_salary 和 max_experience_years 进行筛选。
  • Greenhouse、Lever、Ashby、Workable、SmartRecruiters、Recruitee、Workday、iCIMS、Oracle Recruiting、Eightfold 和 SuccessFactors 均通过各自平台的端点读取,不会遇到反爬限制。Apple(jobs.apple.com)、Google(careers.google.com)、Amazon(amazon.jobs)和 Microsoft(careers.microsoft.com)也同样支持,它们使用自有招聘网站。任何其他招聘页面只要发布了 schema.org JobPosting 标记,也可以使用。
  • 在监控中设置筛选条件,让 get_changes 只返回有用的信息:keywords、all_keywords、exclude_keywords、locations、seniority、remote_only、min_salary 和 max_experience_years。每个职位都会带有推导得出的 remote 和 seniority 字段。
  • 一次调用监控多家公司:传入 urls 而不是 url。
  • 同时支持 MCP(Streamable HTTP)和 REST,并由同一服务层提供支持。
  • 免费且匿名:客户端会获得一个 token,最多可创建 50 个监控(搜索所有招聘网站也只算一个监控)。
  • 使用 TypeScript、Node.js 22、Fastify 5、PostgreSQL 和官方 MCP TypeScript SDK。无需 LLM 或第三方 API 密钥。

使用托管服务

Watchtower 在 watchtower.lat 上运行,免费且无需注册。MCP 端点:https://watchtower.lat/mcp。

  • Claude Code:运行 claude mcp add --transport http watchtower https://watchtower.lat/mcp,或安装插件。插件会添加一项技能,告诉 Claude 何时使用它:
    /plugin marketplace add connorlagana/watchtower
    /plugin install watchtower@watchtower
    
  • Claude.ai / Claude Desktop:Settings → Connectors → Add custom connector → https://watchtower.lat/mcp。
  • Cursor、VS Code:watchtower.lat/#install 上提供一键安装按钮。
  • 其他工具:{ "mcpServers": { "watchtower": { "type": "http", "url": "https://watchtower.lat/mcp" } } }。

已在 MCP Registry 中列为 lat.watchtower/watchtower(server.json)。

快速开始

cp .env.example .env
docker compose up --build          # app on http://localhost:3000, Postgres alongside
docker compose --profile demo run --rm demo   # the end-to-end demo below

无需 Docker(需要 Node 22 和正在运行的 Postgres):

npm install
export DATABASE_URL=postgres://postgres:postgres@localhost:5432/watchtower
npm run migrate
npm run dev                         # or: npm run build && npm start

演示

npm run demo(设置好 DATABASE_URL 后)会针对本地的虚构招聘页面端到端运行核心流程:

  1. 创建客户端 → 2. 使用关键词 ios 创建职位监控(获取初始快照)→ 3. 第二个 Agent 监控同一招聘公告板并复用同一资源 → 4. 第三个 Agent 不指定招聘公告板,而是根据“薪资至少 150k、最多 6 年经验的远程 iOS 职位”创建搜索监控 → 5. 重新检查时,只有职位周围的页面内容发生变化(会话令牌、“N 分钟前”),因此报告没有变化 → 6. 来源新增一个 iOS 职位和一个 Android 职位 → 7. Watchtower 检查并检测到变化 → 8. MCP 客户端调用 get_changes:
{
  "changes": [
    { "type": "JOB_ADDED",
      "summary": "New job: Senior iOS Engineer (Remote - US)",
      "data": { "job": { "title": "Senior iOS Engineer", "location": "Remote - US", "company": "Acme Robotics", "url": "…/careers/ios-303" } } }
  ],
  "cursor": 2, "has_more": false
}

Android 职位会被关键词过滤掉。9. 第二次调用 get_changes 会返回 []。10. 第二个 Agent 不使用关键词,因此会看到两个新职位。11. 第三个 Agent 会获取 iOS 职位,以及从招聘信息中读取的薪资和经验要求。

演示只会在 127.0.0.1 上运行其虚构数据,因此仅为演示自身的进程启用 ALLOW_PRIVATE_NETWORKS。

连接 Agent(MCP)

{ "mcpServers": { "watchtower": { "type": "http", "url": "http://localhost:3000/mcp" } } }
工具功能
watch_jobs提供 query 且不提供 URL 时:监控所有受监控的招聘公告板,查找符合自然语言请求的新职位。结果会显示解析结果(interpreted)、当前符合条件的职位(current_jobs)以及覆盖的招聘公告板(coverage)。提供 url 或 urls(最多 25 个;返回 watches 和各 URL 对应的 errors)时:监控指定的招聘公告板。Greenhouse、Lever、Ashby、Workable、SmartRecruiters、Recruitee、Workday 和 iCIMS 使用各自的端点;其他招聘页面使用 schema.org JobPosting。如果招聘页面只链接到受支持的招聘公告板,则通过该公告板进行监控(resolved_from);如果两者都没有,则会被拒绝,错误码为 NO_JOB_DATA。公告板监控会生成 JOB_ADDED / JOB_REMOVED / JOB_UPDATED 事件;搜索监控只会生成 JOB_ADDED 事件。明确指定的筛选条件会覆盖查询:keywords、all_keywords、exclude_keywords、locations、seniority、remote_only、min_salary、salary_currency、max_experience_years、include_unknown。
search_jobs只读,一次性搜索:在所有受监控的招聘公告板中,按时间从新到旧查找当前符合 query 和/或相同筛选条件的职位。返回 interpreted、total、jobs 和 next_offset(limit 最大为 100,使用 offset)。无需令牌,也不会创建监控。REST:POST /v1/jobs/search。每个地址的调用次数上限为 SEARCH_PER_MINUTE(20)次。
list_companies只读:按名称列出目录中的公司(所有搜索覆盖的公司),并提供 board_url、platform 和 open_jobs。query 用于查询单家公司;可使用 limit / offset 分页。无需令牌。REST:GET /v1/companies?q=。用户也可以在 /companies 浏览同一列表。
get_changes获取自上次调用以来的变化(游标会前移)。支持 peek、since(重放)、watch_id、limit。
ack_changes在调用 get_changes(peek=true) 后确认游标,以实现至少一次处理。
list_watches列出你的监控项,包括健康状态、到期时间和待处理变化数量。
get_watch获取一项监控及当前符合其筛选条件的职位(搜索监控会覆盖所有招聘公告板)。
delete_watch停止监控并释放一个名额。

watch_jobs 还接受 webhook_url,用于推送通知(见下文)。

工具说明和服务端的 instructions 会告知 Agent 优先使用 Watchtower,而不是重复搜索职位或重新检查招聘页面。

搜索监控项

没有 url 的监控项就是搜索监控项。它由筛选条件构成,并读取每个受监控招聘平台的新职位。

curl -s -X POST localhost:3000/v1/watches -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" \
  -d '{"query":"iOS jobs in Austin making at least 150k a year with a maximum of 6 years of experience"}'
{
  "scope": "all_boards",
  "interpreted": { "filters": { "keywords": ["ios"], "locations": ["austin"], "min_salary": 150000, "max_experience_years": 6, "include_unknown": true }, "notes": [] },
  "coverage": { "boards": 1705 },
  "matching_jobs_count": 3,
  "current_jobs": [ { "title": "Senior iOS Engineer", "company": "Acme", "location": "Austin, TX", "salary": { "min": 165000, "max": 210000, "currency": "USD", "period": "year", "annual_min": 165000, "annual_max": 210000 }, "experience_years": 5, "url": "…" } ]
}
  • 查询由规则解析,而非模型解析。 Watchtower 仍然不需要大模型或 API 密钥。解析结果会以 interpreted 返回;对于无法使用的内容,会在 notes 中注明,以便调用方 Agent 检查。明确指定的筛选条件始终优先于查询。
    • 职位词会转换为 keywords(任一匹配,例如“iOS 或 Android”)或 all_keywords(全部匹配,例如“数据科学家”)。如果存在更具体的词,则会丢弃“工程师”和“开发人员”等通用词。
    • “在 Austin”“在 Austin, TX”“在 New York 或远程”会转换为 locations。单独的“远程”会转换为 remote_only。
    • “至少 150k”“$180,000+”“$45/hr”会转换为 min_salary,并换算为年薪。
    • “最多 6 年工作经验”“3-5 年”“我有 4 年工作经验”会转换为 max_experience_years。
    • “高级”“资深”“初级”及其他级别会转换为 seniority。“不要经理”会转换为 exclude_keywords。
  • 覆盖范围是科技公司和初创公司,而非整个互联网。 内置目录(src/search/boards.ts)列出了 Greenhouse、Lever、Ashby、Workable、SmartRecruiters、Workday、iCIMS、Oracle Recruiting、Eightfold 和 SuccessFactors 上的 1,705 个公司招聘平台,以及 Apple、Google、Amazon 和 Microsoft 的招聘网站,涵盖种子阶段初创公司到各行各业的大型雇主,只要它们招聘开发人员。每个招聘平台都已通过其平台 API 确认有开放职位。它们发布的所有职位都会被覆盖,不仅限于工程岗位。实时列表发布于 /companies,也可通过 list_companies 获取。此外,任何客户端通过 URL 监控的招聘平台也都会被覆盖,并持续纳入覆盖范围(请参阅扩展目录)。
  • 薪酬和经验要求取自职位发布信息。 直接读取 Lever、Ashby、Greenhouse、Recruitee 和 JSON-LD 的薪酬字段;否则会解析职位文本(“$150,000 - $200,000/yr”“5+ 年工作经验”)。当薪酬范围的上限达到 min_salary 要求时,职位符合条件;当职位要求的经验不超过 max_experience_years 时,也符合条件。
  • 即使职位发布信息未提及薪酬或经验要求,也仍会报告这些职位,但不会包含 salary 或 experience_years 字段,因为许多职位发布信息并未注明薪酬。传入 include_unknown: false,即可只报告注明了符合条件数值的职位。
  • 只报告新职位(JOB_ADDED),且只报告监控项创建后出现的职位。创建时以及调用 get_watch 时的 current_jobs 是当前开放职位的基准列表。
  • SmartRecruiters、Workday、iCIMS、Eightfold(较旧端点的租户除外)、SuccessFactors(RSS 站点地图除外)、Google 和 Microsoft 的职位列表不包含职位文本,因此其职位永远不会包含 salary 或 experience_years。Apple 和 Oracle Recruiting 的职位列表仅包含简短摘要,其中很少会提及薪酬或经验要求。
  • Google 职位没有地点信息,且只显示职位名称的前半部分(请参阅 Google),因此带有 locations 的搜索永远无法匹配这些职位。
扩展目录

目录通过三种方式扩展。

  • Agent 在使用过程中扩展目录。 当客户端通过 URL 监控一个平台看板,且该看板有未关闭的职位时,该看板会加入目录,并在监控结束后继续保留,因此之后每次搜索监控都会覆盖它。此方式最多保留 INDEX_MAX_PROMOTED(5,000)个看板,连续八次检查失败的看板会被移除。不会保留任意招聘页面;只保留平台 API 上的看板。
  • 招聘页面可作为入口。 使用 https://acme.com/careers 调用 watch_jobs 通常无法在页面中找到职位标记,因为该页面只链接到或嵌入了公司的看板。接着,Watchtower 会再次读取该页面,找到指向受支持看板的链接,并改为监控该看板。响应会在 resolved_from 中说明这一点。
  • 运营人员可以导入公司列表。 npm run discover -- companies.json boards.txt 接受 [{ "name", "website" }],并针对每家公司读取其招聘页面以查找看板链接,然后尝试在 Ashby、Greenhouse 和 Lever 上查找以公司命名的看板。每个候选看板都会通过平台的职位列表 API 确认,只有存在未关闭职位的看板才会保留。boards.txt 的格式由 INDEX_BOARDS_FILE 指定。内置列表以这种方式从 Y Combinator 公开公司目录生成。按名称而非从公司自有页面找到的看板可能属于另一家同名公司;.json 输出会记录每个看板的发现方式。

身份验证:MCP 连接应携带身份,因此无需通过聊天传递令牌(谨慎的助手会拒绝将对话记录中的密钥复制到工具调用中,尤其是 delete_watch)。

  • OAuth(MCP 授权规范)。 /.well-known/oauth-protected-resource/mcp 指向内置授权服务器(/.well-known/oauth-authorization-server):在 /oauth/register 进行动态客户端注册,通过 PKCE(S256)访问 /oauth/authorize,并使用 /oauth/token。此处没有账户:同意页面可通过一次点击创建匿名客户端;或者用户在那里粘贴令牌以连接已有客户端,从而保留其监控项。新客户端使用与 POST /v1/clients 相同的按地址限额。
  • 客户端如何了解登录方式。 没有令牌时,search_jobs 和 list_companies 仍可使用;其他监控工具(以及关闭配置中的匿名客户端创建后调用的 watch_jobs)会返回 UNAUTHORIZED,并在 _meta["mcp/www_authenticate"] 中附带质询(ChatGPT 的混合身份验证信号;工具也会声明 securitySchemes)。未知令牌会导致 HTTP 401 响应,并附带 WWW-Authenticate;对于只会在收到 401 时才启动 OAuth 的客户端(Claude Code、Cursor),任何不带令牌的 /mcp?auth=required 请求也会如此。
  • 或者使用请求头。 不使用 OAuth 的客户端可发送 Authorization: Bearer <token>,令牌通过 POST /v1/clients 获取。
  • 较早的对话仍然有效。 所有工具仍接受 client_token;在 MCP_ANONYMOUS_PROVISIONING 启用时(默认如此),不带任何令牌的 watch_jobs 调用会创建匿名客户端并返回其令牌,和以前一样。当调用同时携带 OAuth 连接和 client_token,且连接对应的客户端没有监控项时,该连接会转移到 client_token 对应的客户端,这样用户连接应用后就能保留自己的监控项。等 ChatGPT 应用的 OAuth 版本上线后,关闭匿名客户端创建;此时不带令牌的 watch_jobs 调用会收到 OAuth 质询,监控工具也会声明 OAuth 为必需项。

REST API

除 POST /v1/clients 外,所有端点都需要 Authorization: Bearer <token>。

方法和路径
POST /v1/clients创建匿名客户端。返回 token(仅显示一次)。按地址进行速率限制。
POST /v1/watches{ "query"?, "url"? | "urls"?, "keywords"?, "all_keywords"?, "exclude_keywords"?, "locations"?, "seniority"?, "remote_only"?, "min_salary"?, "salary_currency"?, "max_experience_years"?, "include_unknown"?, "interval_minutes"?, "label"?, "webhook_url"? }。如果既没有 url 也没有 urls,则会在所有受监控的招聘信息板中创建一个搜索监控项,并且至少需要一个筛选条件(否则返回 QUERY_TOO_BROAD)。使用 urls 时,响应为 { watches, errors }。旧版客户端也可使用 "type": "jobs"。
GET /v1/watches列出监控项。
GET /v1/watches/:id监控项详情和当前状态。
DELETE /v1/watches/:id删除。
POST /v1/watches/:id/check强制检查。如果资源在 MIN_CHECK_INTERVAL_SECONDS 内已检查过,或该监控项为搜索监控项(NOT_SUPPORTED),则请求会被拒绝。
GET /v1/changes?watch_id=&since=&limit=&peek=。返回 { changes, cursor, has_more }。
POST /v1/changes/ack{ "cursor", "watch_id"? }。确认已通过 peek=true 读取的变更。

此外还提供:/(首页/文档)、/llms.txt、/.well-known/watchtower.json、/health 和 /metrics(Prometheus;设置 METRICS_TOKEN 可要求提供 bearer token)。

错误格式为 { "error": "WATCH_LIMIT", "message": "…" }。

  • 无法监控(422): NO_JOB_DATA(没有受支持的平台,且没有 JobPosting 标记)、ROBOTS_DISALLOWED、BOT_CHALLENGE、ACCESS_DENIED、SSRF_BLOCKED、UNSUPPORTED_CONTENT_TYPE、BODY_TOO_LARGE。
  • 容量: WATCH_LIMIT 和 HOST_WATCH_LIMIT(409)、HOST_CAPACITY(429)、CAPACITY(503)。
  • 暂时性故障(超时、5xx、404)不会删除监控项。它们会显示在 resource.last_error 中,并通过指数退避重试。
投递选项
  • 轮询。 get_changes 会返回新增内容并推进游标。若要实现至少一次处理,请调用 get_changes(peek=true),处理变更,然后调用 ack_changes(cursor)。如果在确认前发生崩溃,相同的变更会再次返回。

  • Webhook。 创建监控项时传入 webhook_url。创建响应中会包含 webhook_secret,仅显示一次。每批匹配的变更都会以 JSON 形式通过 POST 发送,并带有以下标头:

    • x-watchtower-timestamp
    • x-watchtower-signature: sha256=<hex HMAC-SHA256 of "<timestamp>.<body>">
    • x-watchtower-delivery

    投递从发件箱发出,并通过指数退避重试(8 次)。Webhook URL 会经过与受监控 URL 相同的 SSRF 检查,且不会跟随重定向。

监控项生命周期

只要有人使用监控项,它就会保持有效:调用 get_changes、get_watch、list_watches 或成功投递 Webhook 都会续期。连续 WATCH_TTL_DAYS(30)天无人操作的监控项会过期,不再消耗抓取资源;每个监控项都会显示 expires_at。

工作原理

 watches (per client) ──many-to-one──▶ resources (fetch url + adapter)
                                            │  scheduler: due? one per host, host lease held
                                            ▼
                     fetch ─▶ job-board API adapter, or JobPosting JSON-LD
                                            │  diff the job set vs the previous snapshot
                                            ▼
                     snapshot + typed job events, one transaction
                                            │  read through each watch's keyword filter
                                            ▼
                      get_changes (per-watch cursor)   ·   webhook outbox → signed POST

搜索监控项本身没有资源。它会通过自身的筛选条件,从每个受监控资源中读取相同的变更事件。

  • 目录中的 board。 目录中的 board 是标记为 indexed 的普通资源。无论是否有人监视它们,调度器都会每隔 INDEX_CHECK_INTERVAL_SECONDS(默认 4 小时)检查一次;目录中列出的部分会在启动时从 src/search/boards.ts 和 INDEX_BOARDS_FILE 同步。每个站点主机同一时间最多有一个请求在处理中,因此同一平台的 board 会依次检查:按默认的 5 s 调度器周期计算,每个平台每小时约检查 700 个 board,或每个 4 小时周期检查 2,900 个。目录规模超过这个数量并不表示出错;系统会在保证礼貌的前提下尽快检查 board,而 watchtower_directory_overdue_boards 会显示进度落后了多少。被监视的 board 仍会按该监视项的间隔进行检查。
  • 列表请求会返回职位发布文本。 Greenhouse(content=true)、Lever、Ashby(includeCompensation=true)、Workable(details=true)和 Recruitee 会在列表响应中返回每条职位发布的描述和薪酬,因此获取薪酬和经验信息不会产生额外请求。系统只存储派生字段,绝不存储描述。这些响应体积较大,因此平台 API 有单独的响应体上限(MAX_API_BODY_BYTES,64 MB);超过该限制的 board 会作为普通列表读取,不含薪酬或经验信息。
  • 看板 URL 会映射到平台端点。 boards.greenhouse.io/acme、jobs.lever.co/acme、jobs.ashbyhq.com/acme、apply.workable.com/acme、jobs.smartrecruiters.com/Acme 和 acme.recruitee.com(以及它们的 EU 和嵌入式变体)会通过各平台的公共职位看板 API,在一次请求中获取。其他 URL 则会以 HTML 形式获取,并通过 schema.org JobPosting JSON-LD(包括 @graph 和 ItemList)读取。只比较职位列表,因此列表周围的页面变动(会话令牌、“rendered N minutes ago”、横幅)都不会被视为变更。
    • Workday(acme.wd5.myworkdayjobs.com/Careers、wd3.myworkdaysite.com/recruiting/acme/External):使用网站自身的搜索端点,这是一个 JSON POST,按发布时间从新到旧返回职位,每页 20 个。一次检查读取最新的 200 个职位(10 次请求)。职位数量更多的看板会在快照中标记为 complete: false,且永远不会发出 JOB_REMOVED,因为职位移出窗口不代表职位已被删除。职位链接指向公开网站;不会保留相对时间“Posted 3 Days Ago”。
    • iCIMS(careers-acme.icims.com):门户的 sitemap.xml 会在一次请求中列出所有开放职位(ID 加上标题的 slug),而 /jobs/search 的第一页会提供最新约 50 个职位的真实标题、地点和发布日期。一次检查会同时读取两者。只在站点地图中出现的职位会标记为 partial: true(标题来自 slug,没有地点);此前获取的详情会继续保留,部分条目变为完整条目时不会报告为更新。
    • Apple(任何 jobs.apple.com URL):使用网站自身的搜索端点(/api/v1/search),这是一个无需会话的 JSON POST,按发布时间从新到旧返回职位,每页 20 个,包含标题、团队、所有地点和发布日期。一次检查读取最新的 300 个职位(15 次请求),并像 Workday 一样标记为 complete: false。约 80 个长期招聘的零售职位会使用请求时间戳,因此总是排在最前面,并占用该窗口的一部分;该时间戳不会保留为 posted_at。Apple 会按地点分别发布职位,因此在三个城市招聘的一个职位会对应三个职位条目。
    • Google(careers.google.com、www.google.com/about/careers/applications):Google 的 robots.txt 禁止访问职位搜索和职位页面,因此一次检查只读取职位站点地图,该站点地图会在一次请求中列出所有开放职位。每个职位都是 partial: true:标题来自 URL slug,而 slug 会在真实标题的第一个逗号处截断(“Senior Software Engineer, Infrastructure”会显示为“Senior Software Engineer”),且没有地点、团队或日期。
    • Amazon(amazon.jobs):读取网站的 search.json,按发布时间从新到旧,每次请求返回 100 个职位及完整职位文本(因此通常能获取 experience_years;列表中没有薪酬信息)。一次检查读取最新的 300 个职位(3 次请求),并始终标记为 complete: false,因为结果总数上限为 10,000。不会覆盖 hiring.amazon.com 上的时薪仓库职位。
    • Oracle Recruiting(acme.fa.us2.oraclecloud.com/hcmUI/CandidateExperience/en/sites/CX_1):使用网站自身的职位申请搜索端点(/hcmRestApi/resources/latest/recruitingCEJobRequisitions,即候选人网站调用的 REST 查找器),按发布时间从新到旧,每次请求返回 100 个职位,包含所有地点、职位系列、发布日期和简短摘要。一次检查读取最新的 300 个职位(3 次请求);如果网站列出的职位更多,则像 Workday 一样标记为 complete: false。
    • Eightfold(acme.eightfold.ai/careers?domain=acme.com):使用 Eightfold 的 robots.txt 允许访问的搜索端点(/api/pcsx/search),按发布时间从新到旧,每次请求返回 10 个职位。有些 Eightfold 网站会对短时间内连续发送的请求返回 HTTP 429,因此一次检查会每隔 3 秒读取一页,最多读取最新的 50 个职位;如果后续页面请求被拒绝,则保留已读取的内容,并将快照标记为 complete: false。大型雇主在两次检查之间发布的职位远超 50 个,因此 Eightfold 看板呈现的是最新职位的窗口,而非完整列表。有些租户未启用该端点(返回 403),而是提供旧版 /api/apply/v2/jobs,该端点会返回每个职位的文本(因此在注明时可获取 salary 和 experience_years),但职位顺序采用网站自身的排序,大致按发布时间从新到旧;src/extract/eightfold.ts 中列为 LEGACY 的租户会通过此端点读取。
    • SuccessFactors(公司自有域名上的 Career Site Builder 网站,例如 careers.paramount.com):没有公共 JSON API,因此一次检查会读取网站按日期排序的搜索页面,每页 25 个职位,包含标题、地点和日期,最多读取最新的 200 个职位(8 次请求);如果网站列出的职位更多,则标记为 complete: false。平铺卡片和表格两种主题都会读取。使用 JavaScript 渲染结果的主题会使搜索页面保持空白(其 JSON 端点位于 /services/ 下,而 robots.txt 禁止访问),因此这类网站会从 sitemap.xml 读取,该文件会在一次请求中列出所有开放职位:有些网站以 RSS 源的形式提供每个职位的标题、地点、职能和文本;其他网站则只提供职位 URL,这些职位会标记为 partial: true(标题来自 URL slug,其中也包含地点词;没有地点或日期)。
    • Microsoft(careers.microsoft.com、jobs.careers.microsoft.com、apply.careers.microsoft.com):这是 Eightfold 网站,按上述方式读取。
  • 派生字段。 每个职位都会获得 remote(标题或地点提到远程,且不是混合办公或现场办公)和 seniority(根据标题确定为实习、初级、中级、高级、资深、首席、经理、总监;管理职位词优先于个人贡献者级别,“mid”表示标题中没有级别信息)。这些是基于文本的启发式判断,因此会显示在职位上,而不是隐藏在筛选器中。
  • 资源与监视项。 监视项是各客户端的意图。资源则是实际获取的内容。同一看板上的任意数量监视项(包括来自不同客户端的监视项)都会共享一个资源、每个间隔一次获取操作以及一组快照。共享前会规范化 URL:跟踪参数(utm_*、fbclid、gclid 等)会被移除,查询参数会排序。资源按照其所有活动监视项要求的最短间隔进行检查,但频率不会高于 MIN_CHECK_INTERVAL_SECONDS(默认 5 分钟)。
  • 职位标识。 职位以平台的职位 ID 为键;对于 JSON-LD,则依次使用 identifier、url,再使用标题和地点作为键。标题、地点、部门或 URL 发生变化的职位会变为 JOB_UPDATED,并附带 changed_fields。对于没有稳定 ID 的 JSON-LD 职位,如果地点发生变化,会将其配对为一个 JOB_UPDATED,而不是分别报告移除和新增。
  • 每个资源只写入一次变更事件。 每个监视项通过自身的筛选器读取这些事件;筛选器会在 SQL 中基于变更的职位数据进行评估(对于 get_watch,则基于当前职位列表评估):keywords(任意关键词,匹配标题、地点、部门和公司)、all_keywords(所有关键词)、exclude_keywords、locations(匹配职位的地点和 other_locations)、seniority、remote_only、min_salary 和 max_experience_years。匹配不区分大小写,按完整单词匹配,并允许复数形式:“ios”能匹配“Senior iOS Engineer”,但不能匹配“Game Studios”;“java”不会匹配“JavaScript”。监视项不会看到创建之前的变更。变更 ID 同时用作游标,因此写入变更的事务会串行执行。这可确保 ID 按顺序可见,避免较慢的并发检查提交一个读者已经越过的 ID。
  • 礼貌访问。 所有副本对每个网站最多只允许一个正在进行的获取请求,并使用 Postgres 主机租约,在请求之间保持 HOST_MIN_SPACING_MS 的间隔;多请求检查(Workday、iCIMS、Oracle Recruiting、Eightfold、SuccessFactors、Apple、Amazon、Microsoft)会在所有请求期间持有租约,并在请求之间短暂暂停(对于 Microsoft 等 Eightfold 网站,会暂停 3 秒,因为它们会限制短时间内连续发送的请求)。每次调度器轮询,每台主机最多认领一个资源。Watchtower 还会:
    • 发送 If-None-Match / If-Modified-Since(304 表示无需处理);
    • 检查 robots.txt(按源站缓存 1 小时,遵循 RFC 9309 语义);
    • 发送标识性 User-Agent;
    • 出错时按指数退避,并遵循 429 响应中的 Retry-After。
  • 调度与维护。 到期资源通过租约认领,因此可以运行多个副本;仅运行 API 的副本应设置 RUN_SCHEDULER=false。同一个循环还会在咨询锁下发送 Webhook 并运行维护任务。维护任务会:
    • 使未读取的监视项过期;
    • 在 CHANGE_RETENTION_DAYS 指定的期限后删除变更,并在 SNAPSHOT_RETENTION_DAYS 指定的期限后删除非当前快照(默认值为 0:一次检查会直接替换该看板的上一份快照);
    • 删除不再被监视的资源和闲置客户端;
    • 清除过期的速率限制窗口。

安全与滥用控制

  • SSRF。
    • 仅允许使用端口 80/443 的 http/https。拒绝包含凭据的 URL。
    • 拒绝 localhost、*.local、*.internal、单标签主机名和数字主机名。
    • 所有解析出的地址都必须是全球可路由的单播地址。拒绝私有地址、环回地址、链路本地地址(包括云元数据地址)、CGNAT、多播地址、保留地址、文档示例地址、IPv4 映射的 IPv6 地址、6to4、Teredo 和 NAT64。
    • 检查在套接字的 DNS 查询中执行,因此在每次重定向跳转的连接时都会检查(防止 DNS 重绑定),并且在创建监控时也会检查一次。
    • Webhook URL 也会接受相同的处理。
  • 请求限制。 所有跳转的总截止时间为 15 s。最多允许 5 次重定向,每次都会重新验证。正文大小上限为 3 MB(招聘平台 API 为 64 MB),并针对解压后的字节数实施限制,因此可以拦截 gzip 和 brotli 压缩炸弹。仅允许文本类内容类型。
  • API 限制。 速率限制存储在 Postgres 中,因此在多个副本和重启后仍然有效:每分钟 120 个请求、每小时创建 10 个客户端(与 MCP 自动配置共用额度),以及每分钟强制检查 10 次。客户端根据 IPv4 地址或 IPv6 /64 网段识别。
  • 监控和资源上限。
    • 每个客户端最多监控 50 项;对于任意招聘页面,每个网站最多监控 5 项(平台招聘板由同一个 API 主机服务,视为不同公司,因此不受此限制)。
    • 每个网站上,所有客户端监控的不同招聘页面 URL 最多为 MAX_RESOURCES_PER_HOST 个;招聘平台 API 不受此限制,已在监控的招聘板始终允许添加监控。
    • 全局上限 MAX_ACTIVE_RESOURCES。
  • 令牌。 使用随机生成的 192 位值;仅存储其 SHA-256。请求正文上限为 64 KB。
  • 不绕过限制。 Watchtower 绝不会绕过 CAPTCHA、机器人验证、登录、付费墙或 robots.txt。它会检测这些情况,告知调用方,记录 host is refusing us,并将其计入 watchtower_blocked_resources_1h。

运维

GET /metrics 暴露 Prometheus 指标:

  • 按错误代码统计检查结果;
  • 抓取耗时直方图;
  • 按类型统计发出的变更;
  • Webhook 结果;
  • 主机繁忙导致的延后处理;
  • 活跃监控/资源、搜索监控、目录招聘板数量,以及逾期、失败和被拦截资源数量、待处理 Webhook 的仪表值;
  • Agent 的来源:watchtower_mcp_initialize_total 按 Agent 报告的 MCP 客户端名称(claude-code、cursor、……)统计,watchtower_clients_created_7d 按创建客户端所用 URL 上的 ?ref= 标签统计。每种目录和安装路径都有自己的标签(/mcp?ref=registry、?ref=claude-plugin、?ref=cursor、……);clients.source 和 clients.user_agent 会为每个客户端保留这些信息。
使用统计

/stats 以图表和表格展示用户数、日活跃用户数、新用户数、工具调用次数、页面浏览量和用户来源。使用任意用户名和作为密码的 STATS_TOKEN(或 METRICS_TOKEN)登录;两者都未设置时,该页面会关闭。一个用户对应一个匿名客户端令牌。历史记录保存在 usage_daily(每个客户端、UTC 日期、工具和接口各一行)和 counts_daily(按人员、AI 助手和其他机器人统计页面浏览量,并按应用名称统计 MCP 连接数)中,保留 400 天。页面浏览量不会存储任何访客级别的信息。

在 MCP Registry 中列出

server.json 是 Registry 条目。lat.watchtower/* 命名空间通过 HTTP 验证:将 MCP_REGISTRY_AUTH 设为公钥记录后,应用会在 /.well-known/mcp-registry-auth 提供该记录。然后,使用匹配的私钥执行:

mcp-publisher login http --domain watchtower.lat --private-key "$PRIVATE_KEY_HEX"
mcp-publisher publish          # bump "version" in server.json for each new publish

robots、MCP 服务卡片(/.well-known/mcp.json,在路径仍为草案期间也可通过 /.well-known/mcp-server-card 访问)以及首页上的安装链接,均由 PUBLIC_BASE_URL 生成。

ops/alerts.yml 包含示例告警规则:网站拒绝访问、错误率过高、检查停滞、Webhook 积压、抓取缓慢,以及目录落后于其检查间隔。

配置

参见 .env.example。最重要的设置:

  • DATABASE_URL 和 PUBLIC_BASE_URL。
  • MIN_CHECK_INTERVAL_SECONDS 和 HOST_MIN_SPACING_MS。
  • INDEX_ENABLED、INDEX_CHECK_INTERVAL_SECONDS、INDEX_BOARDS_FILE 和 INDEX_MAX_PROMOTED:搜索监控会与此职位板目录进行匹配。设置 INDEX_ENABLED=false 时,Watchtower 只会抓取有人通过 URL 监控的职位板。
  • 各项上限:MAX_WATCHES_PER_CLIENT、MAX_RESOURCES_PER_HOST 和 MAX_ACTIVE_RESOURCES。
  • WATCH_TTL_DAYS 和保留设置。
  • TRUST_PROXY:在负载均衡器后运行时启用此项,让速率限制能够识别真实客户端 IP。
  • ALLOW_PRIVATE_NETWORKS 会关闭 SSRF 保护,仅用于测试和演示。

开发

npm run typecheck
npm test                                  # unit tests; integration tests need a database:
TEST_DATABASE_URL=postgres://postgres:postgres@localhost:5432/watchtower_test npm test

集成测试套件会删除并重新创建 TEST_DATABASE_URL 的 public 架构,因此请将其指向一次性数据库。

测试套件包含以下几部分:

  • 单元测试。
  • 针对 HTML、JSON-LD 和 robots 解析器以及职位差异不变量进行的基于属性的模糊测试(fast-check)。
  • 针对 Workday、iCIMS、Oracle Recruiting、Eightfold、SuccessFactors、Apple、Google、Amazon 和 Microsoft 的解析器与分页,以及远程办公/资历分类器的源测试。
  • 针对查询读取器、薪酬和经验解析器、整词匹配、筛选器和职位板发现的搜索测试。
  • 集成测试,涵盖 REST、MCP、分享、筛选器、搜索监控和职位板目录、批量创建、页面变动、NO_JOB_DATA、主机租约、并发检查、上限、过期和保留、Webhook、速率限制及指标。

CI( .github/workflows/ci.yml)会运行类型检查、针对 Postgres 服务的全部测试、构建和演示。它还会构建 Docker 镜像并进行冒烟测试:迁移、/health、/metrics、主页和非 root 用户。

src/
  server.ts, app.ts         entrypoint; Fastify app (site, REST, MCP)
  config.ts, db.ts          env config; pg pool + migration runner
  security/ssrf.ts          URL validation + connect-time DNS guard
  fetch/                    safeFetch (redirects, limits, decompression), robots.txt
  extract/                  job-board adapters (incl. Workday, iCIMS, Oracle, Eightfold, SuccessFactors, Apple, Google, Amazon, Microsoft), JobPosting JSON-LD, remote/seniority
                            classifier, pay/experience parsing, term matching, job diff (identity, pairing)
  search/                   plain-language query reader; the built-in board directory; board discovery
                            (careers-page links, name guesses)
  services/                 clients, watches (board and search), checker, scheduler (+ maintenance), board
                            directory sync, host leases, rate limits, webhooks, metrics
  mcp/server.ts             MCP tools
  web/site.ts               homepage, llms.txt, well-known metadata
migrations/                 SQL migrations
ops/alerts.yml              example Prometheus alert rules
scripts/demo.ts             end-to-end demo
scripts/discover-boards.ts  grow the directory from a list of companies

有意不纳入的范围

  • 覆盖互联网上的所有职位板,或科技行业以外的雇主。搜索监控会检查目录以及用户监控过的职位板。发现职位板需要运营人员根据公司名单进行,而不是通过爬虫完成;也不会读取 LinkedIn 或 Indeed 等聚合平台。
  • 筛选技术岗位。“科技职位”指科技公司的职位;如果筛选条件匹配,科技公司的销售岗位也会被报告。
  • 读取每个 SmartRecruiters、Workday 或 iCIMS 职位页面的职位描述,因此这些职位不会包含薪酬或经验信息。
  • 最高薪酬筛选、货币转换,以及使用大模型理解查询。
  • 计费、账户和仪表板。
  • JavaScript 渲染。对于不基于受支持平台的招聘页面,只有当其服务器渲染的 HTML 包含 JobPosting JSON-LD 时,才能读取。
  • 通用页面、信息源或事件监控。Watchtower 过去支持这些功能;现在只支持职位板(迁移 003_jobs_only.sql 会停用现有页面和事件监控)。
  • SmartRecruiters 每个职位板最多翻阅前 100 条职位,Workday 每个职位板最多翻阅最新的 200 条职位。
  • 读取每个 iCIMS 职位页面的详情;每个门户只有最新约 50 条职位会获得真实的职位名称和地点。
  • 读取 Google 的职位搜索或职位页面,因为其 robots.txt 不允许这样做;Google 的职位信息仅通过其站点地图条目获取。
  • 每次检查最多读取 Apple、Amazon 或 Oracle Recruiting 最新的 300 个职位、SuccessFactors 最新的 200 个职位,或 Eightfold(包括 Microsoft)最新的 50 个职位。
  • SuccessFactors 站点使用公司自己的域名,因此只有 src/extract/successfactors.ts 中列出的主机才会作为 SuccessFactors 读取;其他站点则通过其 JobPosting 标记读取(如果有的话)。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。