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のようにコンテキストに添付します。モデルに「呼ばせる」のはラウンドトリップの無駄です。プロンプトは、ユーザーがメニューから選ぶテンプレートで、モデルは絶対にそれを起動しません。
クイックスタート
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 フィールドでそう明示しているため、モデルもユーザーにその旨を伝えます。
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 Connectors
AI job search MCP — fact-checked jobs, application tracker, alerts. ChatGPT, Claude, Cursor.
Search live startup jobs from Claude, Cursor, or ChatGPT via MCP. Free, no account needed.
Search AI-native jobs, inspect application forms, and fetch free interview-prep resources.
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/Aniruddha-Shukla/week-5-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server