はじめまして
はじめまして。個人で「ハローワークインフォ」という求人検索サイトを作っています。Qiita には初めて投稿します。
ハローワークインフォは、ハローワークインターネットサービスで公開されている全国のハローワーク求人(約114万件・2026年10月時点)を、地域・駅からの距離・職業・賃金・年間休日などの条件で探せるサイトです。データはいまのところ毎日更新しています。
ハローワークインフォは民間の個人が運営する非公式のサイトで、厚生労働省・ハローワークが運営するものではありません。
今回は、このサイトの求人検索を Claude や ChatGPT などの AI アシスタントから使えるようにした MCP サーバーと API について書きます。作ってみると「AI に正しく使ってもらうための作法」がいろいろあったので、そのあたりも紹介します。
作ったもの
| 種類 | URL | 備考 |
|---|---|---|
| MCP サーバー | https://hellowork.info/mcp |
Streamable HTTP・無料・キー不要 |
| JSON の API | https://hellowork.info/api/v1 |
OpenAPI 3.1 の説明あり(/api/v1/openapi.json) |
| 解説ページ | https://hellowork.info/developers | 登録のしかた・頼み方の例・よくある質問 |
MCP のツールは4つで、どれも読み取りだけです。
| ツール | できること |
|---|---|
search_jobs |
求人を探す(駅・市区町村・距離・職業・賃金・年間休日・雇用形態・働き方・年齢など。20件ずつ) |
get_job |
求人1件の要点(主な条件・近い駅・この求人のポイント・相場との比較・応募のしかた) |
find_places |
駅・市区町村・都道府県の候補 |
find_occupations |
「介護」「看護師」などの語から職業の候補 |
使ってみる
Claude(claude.ai・デスクトップアプリ)
Pro・Max の場合の手順です(Free プランも、追加できるコネクタ1つまで使えます)。
- 「Customize」→「Connectors」を開き、「+ Add」→「Add custom connector」
- 名前(例: ハローワークインフォ)と URL
https://hellowork.info/mcpを入れて「Continue」、認証は「No sign in」を選んで「Add」 - チャット欄の左下の「+」→「Connectors」でオンにして、「新宿駅の近くで、年間休日120日以上の介護の求人を探して」と頼む
Team・Enterprise では、オーナーが組織の設定(Organization settings → Connectors)で追加したあと、各自が「Connect」で接続します(Anthropic の手順)。
ChatGPT(Web 版)
- 「Plugins」→「+」→「Add custom MCP server」
- 名前と URL
https://hellowork.info/mcpを入れ、認証はなしのまま、注意事項を確かめてチェックを入れる - 「Create as a plugin」で作って入れたあと、新しいチャットで「@」から選んで頼む
開発者モードを先にオンにする必要はありません。OpenAI の案内では Business・Enterprise・Edu・Pro が対象ですが、Plus でも同じ手順で使えました。スマホのアプリからは追加できないので、Web 版で追加します(OpenAI の手順)。
Claude Code
claude mcp add --transport http hellowork https://hellowork.info/mcp
Cursor など
{
"mcpServers": {
"hellowork": { "url": "https://hellowork.info/mcp" }
}
}
API を直接たたく
# 新宿駅から3km以内・施設介護の職業・年間休日120日以上
curl "https://hellowork.info/api/v1/jobs?station=%E6%96%B0%E5%AE%BF&radius_km=3&occupation=050&min_annual_holidays=120"
構成
- サイト本体は Next.js 16(App Router・Cache Components)と PostgreSQL(全文検索は PGroonga)です。VPS の Docker で動かし、前に Cloudflare を置いています。
- MCP と REST は入口が違うだけで、中身(入力の解釈・検索・答えの形)は同じ関数を使っています。検索そのものもサイトの検索と同じ処理なので、件数はサイトと一致します。
MCP サーバーの実装
MCP サーバーは mcp-handler(Vercel 製)と @modelcontextprotocol/server を使い、状態を持たない Streamable HTTP として Next.js の Route Handler に置いています。
import { withAgentAccess } from "@/lib/agent-api/access";
import { mcpHandler } from "@/lib/agent-api/mcp";
// 利用の回数を記録してから、MCP の処理に渡す
const handle = (request: Request) => withAgentAccess(request, null, "mcp", () => mcpHandler(request));
export { handle as GET, handle as POST, handle as DELETE };
ツールの登録はこんな形です。入力は zod で定義していて、REST の検証・OpenAPI の生成と共通です。
const READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;
export const mcpHandler = createMcpHandler(
(server) => {
server.registerTool(
"search_jobs",
{
title: "ハローワーク求人を探す",
description: SEARCH_JOBS_DESCRIPTION,
inputSchema: searchInputSchema,
annotations: { ...READ_ONLY, title: "ハローワーク求人を探す" },
},
async (input) => run(() => searchForAgent(input, "mcp")),
);
// get_job・find_places・find_occupations も同じ形
},
{ serverInfo: { name: "hellowork-info", version: "1.0.0" }, instructions: INSTRUCTIONS },
);
AI に正しく使ってもらうために工夫したこと
ここからが本題です。動くものはすぐにできましたが、AI に「正しく」使ってもらうには工夫が必要でした。
1. ツールの説明に「呼ぶ順番・結果の扱い・示し方」を書く
最初はツールの説明を「何を返すか」だけにしていました。ところが ChatGPT で試してもらうと、ハローワーク公式サイトの求人票を案内してしまうことがありました。MCP にはサーバー全体の使い方を書く instructions がありますが、クライアントによっては読まれないようです。
そこで、確実に読まれる ツールの説明 に、次の3つを書くようにしました(日本語の利用者が多いので、説明も日本語です)。
【呼ぶ前に】「近所」「家の近く」と頼まれたら、利用者に最寄り駅か市区町村を聞いてから
station か municipality を指定する(利用者の現在地は分からない)。……
【結果の扱い】20件ずつ返す。続きは page で取る。……0件なら、距離を広げる・賃金や
年間休日の下限を下げるなど、条件をゆるめる案を出す。……
【示し方】利用者に求人を示すときは、それぞれの求人の url(ハローワークインフォの
求人ページ)をリンクにしてください。……求人票の URL を求人番号から作ったり……しないでください。
さらに念のため、検索結果の JSON の先頭 にも同じ決まり(link_rule)を入れています。AI は結果を必ず読むので、ここに書いておくのがいちばん効きました。
Claude も ChatGPT も、ツールの説明を 登録したときのまま覚えています(ChatGPT はツールの一覧を「固定した写し」として使います)。説明を直したら、登録し直す(ChatGPT の Business・Enterprise では、管理者がツールの一覧を更新する)必要があります。
2. 選べる値に「寄せて」、寄せたことを返す
サイトの検索は、距離(1・3・5・10・20・30km)や年間休日の下限(105〜125日)が選択式です。AI は「4km」「118日以上」のような値も渡してくるので、近い選択肢に寄せて、そのことを返します。
{
"applied_filters": {
"station": { "name": "新宿", "prefecture": "東京都", "radius_km": 5 },
"min_annual_holidays": 115
},
"notes": ["年間休日の下限は 115 日に寄せました(選べる値: 105・110・115・120・125)"]
}
距離は「広いほう」、賃金や休日の下限は「ゆるいほう」に寄せて、条件に合う求人を取りこぼさないようにしています。AI はこの notes を利用者に伝えてくれます。
3. あいまいな入力は、候補つきのエラーで返す
「府中市」のように同じ名前の市区町村が複数あるときは、勝手に選ばずに候補を返します。
{
"error": {
"code": "ambiguous_municipality",
"message": "「府中市」に当たる市区町村が複数あります。prefecture を付けるか、candidates の名前で指定し直してください。",
"candidates": [
{ "name": "府中市", "prefecture": "東京都" },
{ "name": "府中市", "prefecture": "広島県" }
]
}
}
MCP では isError: true にして返すと、AI が候補を利用者に見せて、選び直してくれます。
REST API と OpenAPI を zod から作る
REST の入力も同じ zod のスキーマを使い、OpenAPI のパラメータは z.toJSONSchema で作っています。説明と検証が食い違わないのがよいところです。
/** zod の object を、クエリのパラメータの並びにする(配列は同じ名前のくり返し) */
function queryParams(schema: z.ZodObject) {
const js = z.toJSONSchema(schema) as JsonSchema;
return Object.entries(js.properties ?? {}).map(([name, s]) => {
const { description, ...rest } = s;
return {
name,
in: "query",
required: js.required?.includes(name) ?? false,
description,
schema: rest,
...(rest.type === "array" ? { style: "form", explode: true } : {}),
};
});
}
ChatGPT の GPTs の Actions に OpenAPI を登録するとき、操作の説明(description)は300字まで という制限がありました。長い決まりは、文書全体の説明(info.description)に寄せています。
回数の上限は、Next.js の proxy(旧 middleware)で、アクセス元ごとに 1分30回・1日1,000回までにしています。超えたら 429 と Retry-After を返します。
おまけ:AI エージェント向けにやったこと
MCP のほかにも、AI エージェントがサイトを見つけて読みやすくするための対応をしました。isitagentready.com の判定では Level 4(Agent-Integrated)です。
-
WebMCP: 対応ブラウザでは
document.modelContext.registerTool()で同じツールを登録します(対応していないブラウザでは何も読み込みません) -
Markdown for Agents:
Accept: text/markdownを付けるか、URL の末尾に.mdを付けると、本文を Markdown で返します(求人ページで HTML 297KB → Markdown 22KB) -
llms.txt、API Catalog(RFC 9727、
/.well-known/api-catalog)、MCP Server Card(/mcp/server-card)、Agent Skills(/.well-known/agent-skills/index.json)、robots.txt の Content Signals
Markdown 版は Cloudflare の機能だと有料プランが必要だったので、サーバーの中で HTML を作ってから変換しています。React が Suspense で後から流し込む部分($RC("B:0","S:0"))は、ブラウザと同じように差し替えてから変換するのがポイントでした。
// React が後から送った部分を、ブラウザと同じように差し替えてから変換する
for (const [, kind, a, b] of html.matchAll(/\$R([CS])\("([^"]+)","([^"]+)"/g)) {
// <template id="B:n"> から境界の終わりまでの「読み込んでいます…」を消し、
// <div hidden id="S:n"> の中身に置き換える
}
ハマったこと
- AI がツールの説明を読み直さない … 前に書いたとおりです。説明を変えたら、登録し直してもらう必要があります。
おわりに
AI アシスタントから使えるようにしてみて、「動く」と「正しく使ってもらえる」のあいだには、説明文の書き方や結果の返し方の工夫が要ると分かりました。MCP サーバーを作る方の参考になればうれしいです。
よければ Claude や ChatGPT に登録して、「〇〇駅の近くで△△の求人を探して」と頼んでみてください。ご意見や不具合は、お問い合わせ か X(@helloworkinfo)までお寄せください。
- ハローワークインフォ: https://hellowork.info
- MCP・API の解説: https://hellowork.info/developers