mcpField
W
Watchtower

Watchtower

リモート
watchtower.latビジネス・マーケティング02 週間前に更新
リポジトリ公式サイト

Watchtower について

Watchtower は、AI エージェント向けにテック企業やスタートアップの求人を監視し、条件に合う新着情報を構造化データで返します。自然言語で条件を指定でき、定期確認、変更取得、Webhook 通知、現在の求人検索にも対応します。

主な機能

  1. 01自然言語クエリで求人監視を作成
  2. 02ディレクトリ内の企業求人ページを対象化
  3. 03勤務地、職位、給与、経験年数で絞り込み
  4. 04追加・削除・更新された求人情報の取得
  5. 05Webhook による変更通知
  6. 06現在の求人をページ分割して検索

活用シーン

  1. 01給与や経験年数の条件に合う新着求人を待つ
  2. 02特定企業の採用ページを監視
  3. 03条件に合う求人の掲載時にエージェントを動かす

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いいえ任意。監視対象を 1 つの求人掲示板または採用ページに限定します。例: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いいえ条件に一致する変更が検出されたときに署名付き POST を受信する、任意の公開 https URL。設定の有無にかかわらず、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 エージェント向けのテック業界の求人モニタリングです。探している条件を一度伝えるだけで、テック企業やスタートアップの求人ボードから、条件に一致する新着求人だけを構造化 JSON で取得できます。

AI エージェントは、求人が掲載されるのを待つのが苦手です。セッションは数分で終わり、採用ページは何度も読み直すには重く、「オースティンで iOS の求人が掲載された?」という確認のために毎日同じ検索をやり直すことになります。Watchtower を使えば、エージェントは一度、平易な言葉で監視条件を作成できます。

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

Watchtower はスケジュールに従ってテック企業やスタートアップの求人ボードを確認し、掲載中の求人を記録します。get_changes は条件に一致する新着求人だけを返し、新着があったときは Webhook でエージェントを起動できます。また、エージェントは 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 のフィールドが含まれます。
  • 1 回の呼び出しで複数の企業を監視できます。url の代わりに urls を渡してください。
  • MCP(Streamable HTTP)と REST に対応し、同じサービスレイヤーを基盤としています。
  • 無料で匿名利用できます。クライアントにはトークンが発行され、最大 50 件の監視条件を作成できます(すべての求人ボードを検索する場合も 1 件の監視条件として数えます)。
  • 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:設定 → コネクタ → カスタムコネクタを追加 → 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

デモ

DATABASE_URL を設定した状態で npm run demo を実行すると、ローカルのフィクスチャ求人ページを使って、主要な一連の処理を最初から最後まで実行します。

  1. クライアントを作成 → 2. キーワード ios で求人ウォッチを作成(初回スナップショットを取得) → 3. 2つ目のエージェントが同じ求人ボードをウォッチし、同じリソースを再利用 → 4. 3つ目のエージェントが求人ボードを指定せず、「リモートの iOS 求人で、給与が少なくとも 150k、経験年数が最大 6 年」という条件から検索ウォッチを作成 → 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. 2回目の get_changes 呼び出しは [] を返します。10. キーワードを指定していない2つ目のエージェントには、新しい求人が両方表示されます。11. 3つ目のエージェントには、掲載内容から読み取られた給与と経験年数とともに、iOS 求人が表示されます。

デモではフィクスチャを 127.0.0.1 で実行するため、デモ自身のプロセスに限って ALLOW_PRIVATE_NETWORKS を有効にします。

エージェントの接続(MCP)

{ "mcpServers": { "watchtower": { "type": "http", "url": "http://localhost:3000/mcp" } } }
ツール機能
watch_jobsquery を指定し、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 が通知されます。明示的に指定したフィルターは query より優先されます: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_changesget_changes(peek=true) の後にカーソルを確認済みとしてマークし、少なくとも1回の処理を実現します。
list_watchesヘルス状態、有効期限、未処理の変更件数を含む、自分のウォッチ一覧を取得します。
get_watch1件のウォッチと、現在募集中でそのフィルターに一致する求人を取得します(検索ウォッチではすべての求人ボードが対象です)。
delete_watch監視を停止し、枠を解放します。

watch_jobs はプッシュ配信のための webhook_url も受け付けます(下記を参照)。

ツールの説明とサーバーの instructions では、求人検索を再実行したり採用ページを再確認したりする代わりに、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 は引き続き LLM も API キーも必要としません。読み取り結果は interpreted として返され、使用できなかった内容は notes に記載されるため、呼び出し元のエージェントが確認できます。明示的なフィルターは常にクエリより優先されます。
    • 職種を表す語は、(「iOS または Android」のようにいずれかを指定する)keywords、または(「データサイエンティスト」のようにすべてを指定する)all_keywords になります。より具体的な語がある場合、「エンジニア」や「開発者」のような一般的な語は除外されます。
    • 「オースティンで」、「オースティン、TX で」、「ニューヨークまたはリモートで」は locations になります。単独の「リモート」は remote_only になります。
    • 「最低 150k」、「$180,000 以上」、「$45/時」は 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+ years of experience」など)を解析します。給与レンジの上限が 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 を指定した検索では一致しません。
ディレクトリの拡充

ディレクトリは3つの方法で拡充されます。

  • エージェントはディレクトリを使うことで拡充します。 クライアントがURLでプラットフォームのボードを監視し、募集中の求人がある場合、そのボードはディレクトリに追加され、その監視が終了した後も残ります。そのため、それ以降のすべての検索監視で対象になります。この方法で保持されるボードは最大INDEX_MAX_PROMOTED(5,000)件で、8回連続でチェックに失敗したボードは削除されます。任意の採用ページが保持されることはありません。対象となるのは、プラットフォームの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接続にIDを持たせることで、チャットにトークンを通す必要がなくなります(慎重なアシスタントは、特に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)は、_meta["mcp/www_authenticate"]にチャレンジを含むUNAUTHORIZEDを返します(ChatGPTの混合認証シグナルです。ツールはsecuritySchemesも宣言します)。不明なトークンの場合は、WWW-Authenticateを含むHTTP 401が返されます。また、トークンなしで/mcp?auth=requiredにリクエストした場合も同様です。これは、401を受けた場合にのみOAuthを開始するクライアント(Claude Code、Cursor)向けです。
  • ヘッダーを使う方法もあります。 OAuthを使わないクライアントは、POST /v1/clientsで取得したトークンを使ってAuthorization: Bearer <token>を送信します。
  • 以前の会話も引き続き使えます。 すべてのツールは引き続き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 も指定しない場合は、監視対象のすべてのボードを対象とする検索ウォッチが作成され、少なくとも1つのフィルターが必要です(指定がない場合は 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 を設定します)。

エラーは { "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 は新しい変更を返し、カーソルを進めます。少なくとも1回の処理を保証するには、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

検索ウォッチは独自のリソースを持ちません。フィルターを通じて、すべての監視対象リソースから同じ変更イベントを読み取ります。

  • ディレクトリ内のボード。 ディレクトリ内のボードは、indexed フラグが付いた通常のリソースです。スケジューラーは、誰も監視しているかどうかにかかわらず、INDEX_CHECK_INTERVAL_SECONDS(デフォルトでは4時間)ごとにボードを確認します。また、ディレクトリに掲載される内容は、起動時にsrc/search/boards.tsおよびINDEX_BOARDS_FILEから同期されます。プラットフォームのホストごとに同時に処理されるリクエストは1件のみのため、各プラットフォームのボードは順番に確認されます。デフォルトの5秒のスケジューラーティックでは、プラットフォームごとに1時間あたり約700件、つまり4時間のサイクルで約2,900件を確認できます。ディレクトリがこの件数を超えていても問題ありません。礼儀を保てる範囲でできるだけ速くボードを確認し、watchtower_directory_overdue_boardsで遅延状況を確認できます。監視対象のボードは、その監視間隔でも引き続き確認されます。
  • 一覧取得リクエストには求人情報の本文が含まれます。 Greenhouse(content=true)、Lever、Ashby(includeCompensation=true)、Workable(details=true)、Recruiteeは、各求人情報の説明と給与を一覧レスポンスで返すため、給与や経験年数の取得に追加のリクエストは必要ありません。保存されるのは派生フィールドのみで、説明文は保存されません。これらのレスポンスは大きいため、プラットフォームAPIには専用の本文サイズ上限(MAX_API_BODY_BYTES、64 MB)が設定されています。ボードがこの上限を超える場合、給与や経験年数を含めず、通常の一覧として読み込まれます。
  • ボード 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 から 1 回のリクエストで取得します。それ以外の URL は HTML として取得し、schema.org の JobPosting JSON-LD(@graph や ItemList を含む)を読み取ります。比較するのは求人リストのみなので、周囲のページの変化(セッショントークン、「N 分前に表示」などの表示、バナー)は変更として検出されません。
    • Workday(acme.wd5.myworkdayjobs.com/Careers、wd3.myworkdaysite.com/recruiting/acme/External):サイト独自の検索エンドポイントを使用します。新しい順に求人を返す JSON POST で、1 ページあたり 20 件です。1 回のチェックで最新の 200 件を読み取ります(10 リクエスト)。求人がそれより多いボードでは、スナップショットに complete: false が設定されます。表示範囲から求人が外れても削除ではないため、JOB_REMOVED は発生しません。求人リンクは公開サイトを指し、「3 日前に掲載」のような相対表示は保持しません。
    • iCIMS(careers-acme.icims.com):ポータルの sitemap.xml には、募集中の全求人が 1 回のリクエストで列挙されています(ID とタイトルのスラッグ)。また、/jobs/search の最初のページには、最新の約 50 件について正式なタイトル、勤務地、掲載日が表示されます。1 回のチェックで両方を読み取ります。サイトマップでしか見つからない求人には partial: true が設定されます(タイトルはスラッグから取得し、勤務地はありません)。以前に取得した詳細情報は引き継がれ、不完全な求人情報が完全になっても更新として報告されません。
    • Apple(jobs.apple.com のすべての URL):サイト独自の検索エンドポイント(/api/v1/search)を使用します。セッション不要の JSON POST で、新しい順に、タイトル、チーム、すべての勤務地、掲載日を含む求人を 1 ページあたり 20 件返します。1 回のチェックで最新の 300 件を読み取り(15 リクエスト)、Workday と同様に complete: false が設定されます。常時掲載されている小売関連の求人約 80 件にはリクエスト時刻が設定されるため、常に先頭に並び、取得範囲の一部を占めます。このタイムスタンプは posted_at として保持されません。Apple は勤務地ごとに求人を掲載するため、3 都市で募集中の職種は 3 件の求人になります。
    • Google(careers.google.com、www.google.com/about/careers/applications):Google の robots.txt では求人検索ページと求人ページへのアクセスが禁止されているため、1 回のチェックで読み取るのは、募集中の全職種を列挙した求人サイトマップのみです。すべての求人に partial: true が設定されます。タイトルは URL のスラッグから取得されますが、実際のタイトルの最初のカンマで途切れます(「Senior Software Engineer, Infrastructure」は「Senior Software Engineer」として読み取られます)。勤務地、チーム、日付はありません。
    • Amazon(amazon.jobs):サイトの search.json を使用します。新しい順に、1 リクエストあたり 100 件の求人が、掲載内容全文とともに返されます(そのため通常は experience_years が分かりますが、給与は掲載されていません)。1 回のチェックで最新の 300 件を読み取り(3 リクエスト)、ヒット件数が 10,000 件に制限されているため、常に complete: false が設定されます。hiring.amazon.com の時給制の倉庫職は対象外です。
    • Oracle Recruiting(acme.fa.us2.oraclecloud.com/hcmUI/CandidateExperience/en/sites/CX_1):サイト独自の求人検索(/hcmRestApi/resources/latest/recruitingCEJobRequisitions。候補者向けサイトが呼び出す REST finder)を使用します。新しい順に、すべての勤務地、職種ファミリー、掲載日、短い概要を含む求人が、1 リクエストあたり 100 件返されます。1 回のチェックで最新の 300 件を読み取り(3 リクエスト)、サイトにそれ以上の求人がある場合は Workday と同様に complete: false が設定されます。
    • Eightfold(acme.eightfold.ai/careers?domain=acme.com):Eightfold の robots.txt で許可されている検索エンドポイント(/api/pcsx/search)を使用します。新しい順に、1 リクエストあたり 10 件返されます。Eightfold のサイトによっては短時間に連続してリクエストすると HTTP 429 が返されるため、1 回のチェックでは 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 がないため、1 回のチェックでは、日付順に並べられたサイトの検索ページから、タイトル、勤務地、日付を含む 1 ページあたり 25 件を、最新の 200 件まで読み取ります(8 リクエスト)。サイトにそれ以上の求人がある場合は complete: false が設定されます。タイル形式とテーブル形式の両方に対応しています。JavaScript で検索結果を表示するテーマでは検索ページが空のままになります(その JSON エンドポイントは /services/ 配下にあり、robots.txt でアクセスが禁止されています)。そのため、これらのサイトでは、募集中の全求人を 1 回のリクエストで列挙した sitemap.xml から読み取ります。サイトによっては、各求人のタイトル、勤務地、職種、掲載内容を含む RSS フィードとして提供されます。それ以外のサイトでは求人 URL のみが提供され、その求人には partial: true が設定されます(タイトルは勤務地を示す語も含む URL のスラッグから取得し、勤務地と日付はありません)。
    • Microsoft(careers.microsoft.com、jobs.careers.microsoft.com、apply.careers.microsoft.com):上記と同様に読み取る Eightfold サイトです。
  • 派生フィールド。 すべての求人に remote(タイトルまたは勤務地にリモート勤務と記載され、ハイブリッド勤務または出社勤務ではない場合)と seniority(タイトルから判定したインターン、エントリーレベル、ミッドレベル、シニア、スタッフ、プリンシパル、マネージャー、ディレクター)が設定されます。管理職を示す語は個人貢献者(IC)のレベルより優先され、「mid」はタイトルにレベルの記載がないことを意味します。これらはテキストに基づくヒューリスティックなので、フィルターの内部に隠すのではなく、求人情報として公開しています。
  • リソースとウォッチ。 ウォッチはクライアントごとの監視要求です。実際に取得されるのはリソースです。同じボードに対するウォッチは、クライアント間でいくつあっても 1 つのリソースを共有し、各間隔につき 1 回の取得と、1 組のスナップショットを共有します。共有前に URL は正規化されます。トラッキングパラメーター(utm_*、fbclid、gclid など)は削除され、クエリはソートされます。リソースは、有効なウォッチが要求する最短の間隔でチェックされますが、MIN_CHECK_INTERVAL_SECONDS(デフォルトは 5 分)より頻繁にはチェックされません。
  • 求人の識別。 求人はプラットフォームの求人 ID で識別されます。JSON-LD の場合は、identifier、次に url、最後にタイトルと勤務地を使用します。タイトル、勤務地、部門、URL のいずれかが変わった求人は、changed_fields を含む JOB_UPDATED になります。安定した ID のない JSON-LD の求人で勤務地が変わった場合は、削除と追加ではなく、1 件の JOB_UPDATED として対応付けられます。
  • 変更イベントはリソースごとに 1 回だけ書き込まれます。 各ウォッチは、自身のフィルターを適用して変更イベントを読み取ります。フィルターは変更対象の求人データに対して 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 をコミットすることもありません。
  • アクセス間隔への配慮。 すべてのレプリカを通じて、各 Web サイトへの同時取得は最大 1 件です。リクエスト間に HOST_MIN_SPACING_MS を設ける Postgres のホストリースを使用します。複数リクエストを伴うチェック(Workday、iCIMS、Oracle Recruiting、Eightfold、SuccessFactors、Apple、Amazon、Microsoft)では、すべてのリクエストが終わるまでリースを保持し、その間、短い間隔を空けます(Microsoft などの Eightfold サイトでは、短時間の連続リクエストがレート制限されるため 3 秒)。各スケジューラーティックで要求するリソースは、ホストごとに最大 1 つです。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-mapped IPv6、6to4、Teredo、NAT64 はすべて拒否されます。
    • チェックはソケットの DNS ルックアップ内で実行されるため、接続時にはリダイレクトの各ホップで適用され(DNS リバインディングを防止)、監視の作成時にも一度実行されます。
    • Webhook URL にも同じ制限が適用されます。
  • リクエスト制限。 すべてのホップを通じた全体の期限は 15 秒です。リダイレクトは最大 5 回で、それぞれ再検証されます。本文サイズは 3 MB(求人サイトプラットフォーム API は 64 MB)に制限され、展開後のバイト数に対して適用されるため、gzip や brotli の爆弾も検出されます。テキスト形式のコンテンツタイプのみ許可されます。
  • API 制限。 レート制限は Postgres に保存されるため、レプリカや再起動をまたいで適用されます。120 リクエスト/分、クライアント作成 10 回/時(MCP の自動プロビジョニングと共用)、強制チェック 10 回/分です。クライアントは IPv4 アドレスまたは IPv6 /64 で識別されます。
  • 監視とリソースの上限。
    • クライアントあたり監視は 50 件まで、任意の採用ページはウェブサイトあたりクライアントごとに 5 件までです(プラットフォームの掲示板は 1 つの 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 を示すゲージ;
  • エージェントの流入元:watchtower_mcp_initialize_total はエージェントが報告する 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)をパスワードにしてログインします。どちらも設定されていない場合、ページは無効です。ユーザーとは、匿名のクライアントトークン 1 つを指します。履歴は usage_daily(クライアント、UTC の日付、ツール、インターフェースごとに 1 行)と counts_daily(人間、AI アシスタント、その他のボットによるページビュー数、およびアプリ名別の MCP 接続数)に保存され、400 日間保持されます。ページビューについて、訪問者ごとの情報は保存されません。

MCP Registry への掲載

server.json はレジストリのエントリーです。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 スキーマを削除して再作成するため、使い捨てのデータベースを指定してください。

テストスイートは次の3つの部分で構成されています。

  • 単体テスト。
  • 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 のサイトは企業独自のドメインで運用されているため、SuccessFactors として読み取るのは src/extract/successfactors.ts に記載されているホストのみです。それ以外は、JobPosting マークアップがあれば、それを通じて読み取ります。

コメント

MCP は初めてですか?

プロトコルの概要から最初のサーバーの導入まで、よくある疑問にまとめて答えます。

AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。