Skip to main content
Glama
Aniruddha-Shukla

Career Copilot MCP

Career Copilot MCP

2,253件の米国データアナリスト求人を扱うMCPサーバー — そしてスクラッチから書いたMCPクライアントも。プロトコルを魔法扱いしなくなる最も手っ取り早い方法は、実際に実装してみることだからだ。

Python MCP Tests

私の Learning in Public ロードマップの第5週。Week 2 はノートブックで給与予測モデルを訓練し、Week 3 はモデルを非同期FastAPIサービスの背後に置いて、人間のが呼べるようにしました。今週は、AIエージェントが呼ぶとしたら何が必要になるか、です。


これは何か

MCPの3つのプリミティブ全てを動かす、意図的に小さなサーバーです。大半のサンプルはツールしか提供しませんが、それでは「ステップが増えた関数呼び出し」にMCPを貶めてしまいます。

プリミティブ

制御する主体

このサーバーの実装

Tools

モデル

search_jobs, salary_benchmark, skill_demand

Resources

クライアントアプリ

market://snapshot, market://locations

Prompts

人間

career_gap_review

この区別こそが実際のプロトコルです。ツールとは、モデルが自ら選択した引数で呼ぶと決定するものです。リソースは、引数のないアドレス可能な読み取り専用データ — クライアントがGETのようにコンテキストに添付します。モデルに「呼ばせる」のはラウンドトリップの無駄です。プロンプトは、ユーザーがメニューから選ぶテンプレートで、モデルは絶対にそれを起動しません。

クイックスタート

uv sync && uv pip install -e .

SDKもLLMも介在せずに、プロトコル全体が動くところを見る:

uv run python client/raw_client.py --verbose

テストスイートを実行する:

uv run python -m pytest tests/ -q

Claude 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。誰もエラーにせず、誰もアップグレードしません:

クライアントが要求する

サーバーの応答

2026-07-28 (サーバーより新)

2025-11-25

2025-11-25

2025-11-25

2025-06-18

2025-06-18

2024-11-05

2024-11-05

1999-01-01 (無意味)

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 の部分文字列例外は意図的に残しています。mysqlpostgresql も実際に SQL を指すからです。


全てのテストには居場所がある

Week 3からのルールを踏襲しています: カバー対象のコードを削除してもまだ通るテストは、何もテストしていなかったということ。scripts/verify_tests.py は各修正を消し、スイートが気付くかどうかを確認します。

uv run python scripts/verify_tests.py

取り除いた修正

スイートが気付く

単語境界スキルマッチ

はい

制限クランプ (1 ≤ limit ≤ 25)

はい

未知の場所に対する実行可能なエラー

はい

切り詰めの報告

はい

readOnlyHint注釈

はい

ツール本体内の迷子 print()

いいえ — それがこの発見

実行すると何も検証していない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 it

market.py には意図的に MCP のインポートがありません。プロトコル層はプレーンな関数の薄いアダプターでいるべきであり、同じロジックを HTTP や CLI で提供しても、それを触れずに済みます。

データ

data/DataAnalyst.csv — 2,253件のGlassdoorデータアナリスト求人。Weeks 1〜2と同じデータセットです。2020年の米国都市圏スナップショット:最新の市場データではなく、過去のリファレンスです。サーバーは instructions フィールドでそう明示しているため、モデルもユーザーにその旨を伝えます。

-
license - not tested
Not graded
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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

View all MCP Connectors

Latest Blog Posts

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