Career Copilot MCP
Career Copilot MCP
2,253件の米国データアナリスト求人を扱うMCPサーバー — そしてスクラッチから書いたMCPクライアントも。プロトコルを魔法扱いしなくなる最も手っ取り早い方法は、実際に実装してみることだからだ。
私の Learning in Public ロードマップの第5週。Week 2 はノートブックで給与予測モデルを訓練し、Week 3 はモデルを非同期FastAPIサービスの背後に置いて、人間のが呼べるようにしました。今週は、AIエージェントが呼ぶとしたら何が必要になるか、です。
これは何か
MCPの3つのプリミティブ全てを動かす、意図的に小さなサーバーです。大半のサンプルはツールしか提供しませんが、それでは「ステップが増えた関数呼び出し」にMCPを貶めてしまいます。
プリミティブ | 制御する主体 | このサーバーの実装 |
Tools | モデル |
|
Resources | クライアントアプリ |
|
Prompts | 人間 |
|
この区別こそが実際のプロトコルです。ツールとは、モデルが自ら選択した引数で呼ぶと決定するものです。リソースは、引数のないアドレス可能な読み取り専用データ — クライアントがGETのようにコンテキストに添付します。モデルに「呼ばせる」のはラウンドトリップの無駄です。プロンプトは、ユーザーがメニューから選ぶテンプレートで、モデルは絶対にそれを起動しません。
Related MCP server: JobSpy MCP Server
クイックスタート
uv sync && uv pip install -e .SDKもLLMも介在せずに、プロトコル全体が動くところを見る:
uv run python client/raw_client.py --verboseテストスイートを実行する:
uv run python -m pytest tests/ -qClaude Codeに接続する
claude mcp add career-copilot -- uv --directory /absolute/path/to/mcp-week-5 run python -m career_copilot_mcp.server{
"mcpServers": {
"career-copilot": {
"command": "uv",
"args": ["--directory", "/absolute/path/to/mcp-week-5", "run", "python", "-m", "career_copilot_mcp.server"]
}
}
}MCPは魔法ではない
それは、サブプロセスのstdin/stdout上で新しい行区切りのJSONとしてやり取りされるJSON-RPC 2.0であり、合意されたメソッド語彙があるだけです。以下が実際のセッションです — client/raw_client.py --verbose でキャプチャしました(幅の都合で要約):
→ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2026-07-28","capabilities":{},"clientInfo":{"name":"raw-client","version":"0.1.0"}}}
← {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"prompts":{…},"resources":{…},"tools":{…}},"protocolVersion":"2025-11-25","serverInfo":{"name":"career-copilot"}}}
→ {"jsonrpc":"2.0","method":"notifications/initialized","params":{}} // a notification: no id, no reply
→ {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
← {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"search_jobs","description":"Find Data Analyst job postings…","inputSchema":{…},"outputSchema":{…},"annotations":{"readOnlyHint":true}}, …]}}
→ {"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"salary_benchmark","arguments":{"location":"San Francisco, CA","skill":"python"}}}
← {"jsonrpc":"2.0","id":6,"result":{"content":[…],"isError":false,"structuredContent":{"median":92500,"p25":80500,"p75":126000,…}}}このサーバーが使う呼び出しは8つで全容です: initialize, notifications/initialized, tools/list, tools/call, resources/list, resources/read, prompts/list, prompts/get.
ハンドシェイクが相性の作業をやる
クライアントが 2026-07-28 を要求し、サーバーが答えるのは自分が話せる新版の 2025-11-25。誰もエラーにせず、誰もアップグレードしません:
クライアントが要求する | サーバーの応答 |
|
|
|
|
|
|
|
|
|
|
だから、数ヶ月前に書かれたMCPクライアントが今日のサーバーに対しても動くのです。互換性はハンドシェイクの中にあり、あなたのコードの中にはない。
時間がかった4つのこと
1. ツールの description こそがプロンプトである
このツールを起動するかどうか、何を渡すかを決める際、モデルが読むのは説明文だけです。location: str では何も伝わりません。これなら伝わります:
location: US metro in "City, ST" form, e.g. "New York, NY" or "Austin, TX".
A partial name like "Austin" is accepted when it is unambiguous. Read
market://snapshot for the most common values before guessing.説明文は静かに腐っていくため、テストで制約しています:
assert len(tool["description"]) > 80, f"{tool['name']} description is too thin"2. -> dict では出力スキーマにならない
私のツールはtextブロック内のJSON 文字列を返していました。クライアントは json.loads して形を推測せざるを得ませんでした。SDKはこれを見逃してはくれません:
InvalidSignature: Function search_jobs: return type <class 'dict'> is not
serializable for structured output型付き戻り値 (TypedDict) は outputSchema を生成し、それが tools/list でツールとともに届き、結果は structuredContent として返ります — 再解析が必要なテキストではなく、機械可読な値です。
3. エラーは結果であって、クラッシュではない
エージェントは提案に基づいて再試行できますが、沈黙にはできません。だから、未知の場所へは有効な候補を挙げたメッセージを返します:
GXP10
接続は保たれ、isError: true が通常の結果として返り続けます。テストはその後もサーバーが応答し続けることを検証します。
4. モデルはデータの妥当性をチェックできない
これが本当の教訓です。そもそもMCPのバグではなく、MCPが危険にしたデータのバグでした。
Week 2 は単純な部分文字列マッチでスキルを検出しました。 "excel" in 説明文 は "excellent" にもマッチし、"aws" は "laws", "draws", "flaws" にもマッチします。
スキル | 部分マッチ | 単語境界マッチ | 過大計上 |
excel | 1,354 (60.1%) | 903 (40.1%) | +50% |
aws | 275 (12.2%) | 132 (5.9%) | +108% |
spark | 89 | 71 | +25% |
sql | 1,389 | 1,387 | — |
ノートブックの間違った数字は、じっと眺めるだけのグラフです。MCPツールの裏側では、それはモデルが私の名前のついたサーバーで自信たっぷりにユーザーへ伝える数字なります。エラーも例外も、シグナルもない — ただ、丁寧に届けられた誤った回答があるだけです。
SQL の部分文字列例外は意図的に残しています。mysql も postgresql も実際に SQL を指すからです。
全てのテストには居場所がある
Week 3からのルールを踏襲しています: カバー対象のコードを削除してもまだ通るテストは、何もテストしていなかったということ。scripts/verify_tests.py は各修正を消し、スイートが気付くかどうかを確認します。
uv run python scripts/verify_tests.py取り除いた修正 | スイートが気付く |
単語境界スキルマッチ | はい |
制限クランプ ( | はい |
未知の場所に対する実行可能なエラー | はい |
切り詰めの報告 | はい |
| はい |
ツール本体内の迷子 | いいえ — それがこの発見 |
実行すると何も検証していない2つのテストが捕りました:
スキルマッチのテストは、読み込んだデータではなく
SKILL_PATTERNSとの一致を検証していました。正規表現が well-formed であるは証明できますが、パイプラインがそれを使っていたことはしていません。呼び出し点を変えても壊れませんでした。いまは実際の投稿データに対して検証します。stdoutテストは
tools/listを呼ぶだけで、ツール本体内のprint()は実行されませんでした。いまは全ハンドラを実行します。
問題は「が怖い」錯覚
どのMCPガイドも言います: stdio では、あなたの stdout がそのまま通信線です。だから迷子の print() 一つで。テストを書いてみました。 print("ステイray", flush=True) をツール本本に突っ込んでも、テストは通りました — クライアントも動き続けました。
mcp/server/stdio.py が理由を説明します。処理中、transport は fd 1 を主張し、実際の通信線をプライベート fd に複製して、fd 1 を stderr の複製物に向けます。
def _open_stdout_diversion() -> int:
try:
return os.dup(2) # fd 1 now goes wherever stderr goes
except OSError:
return os.open(os.devnull, os.O_WRONLY)端末から端末まで検証済み: stray print は通信線に到達せず、かわりに stderr に落ちます(stdin も同様に /dev/null へ置き換えられるため、ハンドラや子プロセスはプロトコル bytes を飲むのではなく EOF を読む)。
だから stderr へのログは依然正しい — 仕様がそう要求しており、クライアントがサーバーログとして表示するものもそれです。しかし、通常その理由として語られることは、このSDKのこのバージョンにおいては、俗説です。我がテストを壊そうとしなければ、私はその俗説をコメントに載せていたことでしょう。
構成
src/career_copilot_mcp/
market.py data layer — no MCP imports, so the logic is testable without a server
server.py the protocol adapter: 3 tools, 2 resources, 1 prompt
client/
raw_client.py a ~200-line MCP client. No SDK. Speaks JSON-RPC at a subprocess.
scripts/
verify_tests.py deletes each fix, checks the suite notices
tests/
test_market.py the data layer
test_protocol.py spawns the real server and speaks JSON-RPC at itmarket.py には意図的に MCP のインポートがありません。プロトコル層はプレーンな関数の薄いアダプターでいるべきであり、同じロジックを HTTP や CLI で提供しても、それを触れずに済みます。
データ
data/DataAnalyst.csv — 2,253件のGlassdoorデータアナリスト求人。Weeks 1〜2と同じデータセットです。2020年の米国都市圏スナップショット:最新の市場データではなく、過去のリファレンスです。サーバーは instructions フィールドでそう明示しているため、モデルもユーザーにその旨を伝えます。
Available Tools
3 toolssalary_benchmarkARead-only
Get the salary distribution for a slice of the job market.
Returns median, 25th and 75th percentile, min and max — plus how many postings the numbers are based on, which matters because narrow slices get thin fast.
Args: location: US metro in "City, ST" form. Omit for a nationwide figure. skill: One of python, sql, excel, tableau, aws, spark. Omit to include all postings regardless of skill.
Prefer this over calling search_jobs and averaging the results yourself: this uses all matching postings, while search_jobs returns at most 25.
| Name | Required | Description | Default |
|---|---|---|---|
| skill | No | ||
| location | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| max | Yes | |
| min | Yes | |
| p25 | Yes | |
| p75 | Yes | |
| scope | Yes | |
| median | Yes | |
| postings_with_salary | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations declare readOnlyHint=true, which covers the safety profile. The description adds behavioral details beyond that: it warns that narrow slices get 'thin fast' (small sample sizes) and states that it uses all matching postings rather than a limited subset. It does not disclose potential edge cases (e.g., invalid location format) but provides meaningful context beyond the annotation.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is tightly structured: a one-sentence purpose, output summary, clearly labeled Args block, and a final usage recommendation. Every sentence earns its place, and the most important alternative is front-loaded in the last paragraph. No redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a tool with two optional params and an output schema, the description covers the essential context: what it returns, how to invoke it, and when to prefer it over a sibling. The output schema exists and likely details return fields, so the description need not do so exhaustively. The given details are sufficient for an agent to call it correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0%, so the description is the sole source of parameter meaning. It fully compensates: location is specified as 'US metro in "City, ST" form' with an omission default, and skill lists the exact valid values ('python, sql, excel, tableau, aws, spark') with omission behavior. This adds substantial value beyond the bare string schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a specific verb and resource: 'Get the salary distribution for a slice of the job market.' It then details the exact outputs (median, percentiles, min/max, count) and explicitly contrasts with the sibling search_jobs, making the tool's unique contribution unmistakable.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides explicit guidance: 'Prefer this over calling search_jobs and averaging the results yourself' and explains why (uses all postings vs. max 25). It also clarifies how to scope the query via location and skill and when to omit them, giving clear conditions for use.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_jobsARead-only
Find Data Analyst job postings, highest estimated salary first.
Args: skill: Filter to postings whose description mentions this skill. Must be one of: python, sql, excel, tableau, aws, spark. Any other value is rejected. location: US metro in "City, ST" form, e.g. "New York, NY" or "Austin, TX". A partial name like "Austin" is accepted when it is unambiguous. Read market://snapshot for the most common values before guessing. min_salary: Minimum estimated average annual salary in USD, e.g. 90000. limit: How many postings to return, 1-25. Defaults to 5. Ask for more only when the user explicitly wants a long list — each posting costs context.
Returns total_matches (how many postings matched overall) alongside the returned slice, so you can tell the user how much you are not showing them.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| skill | No | ||
| location | No | ||
| min_salary | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| filters | Yes | |
| postings | Yes | |
| returned | Yes | |
| truncated | Yes | |
| total_matches | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Beyond the annotations (readOnlyHint: true, openWorldHint: false), the description discloses several behaviors: results are ordered by estimated salary descending, skill values are restricted to a fixed set with rejection on others, location accepts partial names when unambiguous, min_salary represents average annual USD, limit defaults to 5 with a range of 1-25, and the return includes total_matches alongside the returned slice. It also notes that 'each posting costs context,' which is a valuable operational detail. No contradiction with annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is front-loaded with the core purpose, followed by a clearly structured list of arguments. Each parameter entry is concise yet complete, with examples and caveats. The note about context cost is brief but relevant. No wasted words; the structure aids quick parsing.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool has 4 parameters with no schema descriptions and an output schema (which likely covers the return shape), the description covers all necessary aspects: what it does, how to specify each filter, expected return info (total_matches and slice), and operational nuances. The agent has enough to correctly invoke the tool without ambiguity. The description even prompts reading a snapshot for location values, indicating a richer context.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description carries the full burden for parameter meaning. It explains each parameter in detail: skill lists allowed values and rejection behavior, location specifies format and partial matching, min_salary gives an example value and unit, and limit states range, default, and usage advice. This is exceptional compensation for the missing schema documentation.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a clear verb+resource+scope: 'Find Data Analyst job postings, highest estimated salary first.' It specifies the resource (job postings), the target role (Data Analyst), and the ordering. This distinguishes it from sibling tools like salary_benchmark and skill_demand by topic, even without naming them.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides usage tips for parameters, such as 'Read market://snapshot for the most common values before guessing' for location and 'Ask for more only when the user explicitly wants a long list' for limit. However, it does not explicitly say when to use this tool versus its siblings (salary_benchmark or skill_demand), nor does it mention any exclusions or alternatives. The context is implied by the purpose but not explicitly stated.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
skill_demandARead-only
Show how often each tracked skill appears in postings, and its salary effect.
For each of python, sql, excel, tableau, aws and spark, returns the number of postings mentioning it, its share of the corpus, and the median salary both with and without that skill — so the pay gap is directly comparable.
Takes no arguments; it always covers the whole dataset.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| skills | Yes | |
| total_postings | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, so the safety profile is covered. The description adds valuable behavioral context: it explicitly notes the tool always covers the whole dataset (no filtering), and details the exact metrics returned (count, share, median salary with/without). This goes beyond the annotations and helps the agent understand the scope and output characteristics. No contradictions.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is two short paragraphs: the first immediately states the purpose, the second details the exact output fields, and the third clarifies the no-argument behavior. Every sentence contributes new information—there is no fluff, and the key message is front-loaded.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool has an output schema (the return values are structured) and no parameters, the description covers everything an agent needs to decide whether and how to call it: the exact skills, the metrics, and the fact that it operates on the whole dataset. There are no missing operational details (e.g., rate limits, authorization) and the scope is unambiguous.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
There are no parameters, and the schema confirms an empty properties object. The description repeats that it 'Takes no arguments' and 'always covers the whole dataset', which adds a small semantic meaning (the scope is fixed). Since there are zero parameters, the baseline of 4 is appropriate; the description does not need to explain parameter formatting.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb ('Show') and resource ('how often each tracked skill appears in postings, and its salary effect'), and explicitly enumerates the skills covered (python, sql, excel, tableau, aws, spark). It is clearly distinct from siblings: search_jobs is about filtering postings, and salary_benchmark likely benchmarks salaries, while this aggregates skill demand across the whole dataset.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description states that it takes no arguments and always covers the whole dataset, which implies it's for a global overview. However, it does not explicitly explain when to choose this over the sibling tools (search_jobs, salary_benchmark) or mention any exclusions. The usage context is implied but not directly contrasted with alternatives, leaving the agent to infer.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
3 tool updates
v0.1.0- First observed
salary_benchmark - First observed
search_jobs - First observed
skill_demand
TDQS
Scored across 3 tools
Each tool has a clearly distinct purpose: searching job postings, computing salary benchmarks, and analyzing skill demand. No overlap in functionality; an agent can easily select the right tool based on the user's query.
All tool names follow a consistent lowercase snake_case pattern (search_jobs, salary_benchmark, skill_demand). While the first word is not always a verb, the naming style is uniform and predictable, avoiding any mixing of conventions.
With only 3 tools, the set is tightly scoped to the server's purpose of job market analytics. Each tool covers a distinct high-level capability and none feel redundant, making the count appropriate for the narrow domain.
The tool set covers the core workflows of job searching, salary benchmarking, and skill demand analysis. Minor gaps exist, such as no direct method to fetch a single job posting's full detail beyond search results, but agents can work around this by using the returned data. Overall, the surface is complete for the stated purpose.
Related MCP Connectors
Salary and H-1B disclosed-wage data as agent tools.
Job-posting search (jobicy, HN Who is Hiring, arbeitnow) and US H-1B salary data as agent tools.
MCP for 8,700+ current AI jobs. 13 tools: search, match, salaries, companies, commerce quotes.
CareerProof MCP gives AI agents direct access to a professional-grade career and workforce intelligence platform. Two namespaces: atlas_* for HR/TA teams (candidate evaluation, batch shortlisting, competency scoring, interview generation, JD analysis, custom eval frameworks, research reports) and ceevee_* for professionals (CV optimization, career positioning, salary intelligence, market reports). Backed by RAG knowledge from 50+ premium research sources (McKinsey, BCG, HBR, Gartner, WEF)
Related MCP Servers
- AlicenseAqualityCmaintenanceEnables searching over 1 million enriched job listings from 20,000+ companies directly from MCP-compatible AI tools. Provides tools for job search, company profiles, and AI-powered similar job recommendations with real-time data updates.4118 npm2MIT
- AlicenseNot gradedqualityCmaintenanceEnables job search and scraping across multiple job boards (LinkedIn, Indeed, Glassdoor, etc.) with advanced filtering, directly from Claude Desktop or other MCP clients.6MIT
- FlicenseNot gradedqualityCmaintenanceEnables Claude users to research U.S. labor-market trends through live BLS and FRED data, covering employment, unemployment, wages, job openings, occupational outlook, and industry comparisons.-
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to crawl, analyze, and query job description (JD) data from recruitment sites via MCP, with tools for searching jobs, viewing statistics and portraits, and running local analysis on privately stored data.MIT