Career Copilot MCP
Career Copilot MCP
Ein MCP-Server über 2.253 US-Stellenausschreibungen für Data Analysten – plus ein komplett neu gebauter MCP-Client, denn der schnellste Weg, ein Protokoll nicht mehr als Magie zu betrachten, ist, es zu implementieren.
Woche 5 meiner Learning in Public-Roadmap. Woche 2 trainierte ein Gehaltsmodell in einem Notebook. Woche 3 stellte ein Modell hinter einen asynchronen FastAPI-Dienst, damit ein Mensch es aufrufen konnte. Diese Woche: Was braucht es, damit ein KI-Agent es aufrufen kann?
Worum es geht
Ein bewusst kleiner Server, der alle drei MCP-Primitive durchspielt, denn die meisten Beispiele liefern nur Tools – was MCP stillschweigend auf „Funktionsaufrufe mit Extraschritten“ reduziert.
Primitive | Gesteuert von | In diesem Server |
Tools | das Modell |
|
Resources | die Client-Anwendung |
|
Prompts | der Mensch |
|
Die Unterscheidung ist das eigentliche Protokoll. Ein Tool ist etwas, das das Modell selbst entscheidet aufzurufen, mit Argumenten, die es wählt. Eine Resource sind adressierbare Nur-Lese-Daten ohne Argumente – der Client hängt sie an den Kontext wie einen GET, so etwas vom Modell „aufrufen“ zu lassen, verschwendet einen Roundtrip. Ein Prompt ist eine Vorlage, die der User aus einem Menü wählt; das Modell ruft ihn nie auf.
Related MCP server: JobSpy MCP Server
Quickstart
uv sync && uv pip install -e .Schau dir das gesamte Protokoll im Ablauf an, ohne SDK und ohne LLM im Spiel:
uv run python client/raw_client.py --verboseFühre die Suite aus:
uv run python -m pytest tests/ -qVerbinde es mit 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 ist keine Magie
Es ist JSON-RPC 2.0 als zeilengetrenntes JSON über stdin/stdout eines Subprozesses, mit einem vereinbarten Methoden-Vokabular. Hier ist eine echte Session, aufgezeichnet aus client/raw.py --verbose (aus Platzgründen gekürzt):
→ {"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,…}}}Acht Aufrufe sind die gesamte Fläche, die dieser Server nutzt: initialize, notifications/initialized, tools/list, tools/call, resources/list, resources/read, prompts/list, prompts/get.
Der Handshake übernimmt die Kompatibilität
Der Client fragt nach 2026-07-28. Der Server antwortet 2025-11-25 – die neueste Version, die er selbst spricht. Niemand macht einen Fehler, niemand aktualisiert:
Client fragt | Server antwortet |
|
|
|
|
|
|
|
|
|
|
Genau deshalb funktioniert ein vor Monaten geschriebener MCP-Client immer noch mit einem Server, der heute veröffentlicht wurde. Die Kompatibilität steckt im Handshake, nicht in deinem Code, nicht in deiner Codebasis.
Vier Dinge, die mich Zeit gekostet haben
1. Die Tool-Beschreibung ist der Prompt
Sie ist das Einzige, was das Modell liest, wenn es entscheidet, ob es ein Tool aufruft und was es übergibt. location: str sagt ihm nichts. Das hier schon:
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.Ein Test erzwingt das, denn Beschreibungen veralten still:
assert len(tool["description"]) > 80, f"{tool['name']} description is too thin"2. -> dict liefert kein Ausgabe-Schema
Meine Tools haben einen JSON-String in einem Textblock zurückgegeben. Der Client musste ihn per json.loads parsen und die Struktur erraten. Das SDK lässt dich das nicht überdecken:
InvalidSignature: Function search_jobs: return type <class 'dict'> is not
serializable for structured outputTypisierte Rückgaben (TypedDict) erzeugen ein outputSchema, das mit dem Tool in tools/list mitgeliefert wird, und Ergebnisse kommen in structuredContent zurück – maschinenlesbar, nicht zu komplizierter Text, der neu zu parsen wäre.
3. Ein Fehler ist ein Ergebnis, kein Absturz
Ein Agent kann mit einem Vorschlag einen neuen Versuch machen. Mit keinem Vorschlag nicht es nicht. Deshalb gibt ein unbekannter Standort eine Meldung zurück, die gültige nennt:
No postings found for location 'Bangalore'. This dataset covers US metros only.
Try one of: New York, NY, Chicago, IL, San Francisco, CA, Austin, TX, …Die Verbindung bleibt bestehen, isError: true kommt als normales Ergebnis zurück, und ein Test prüft, dass der Server danach weiterhin antwortet.
4. Das Modell can die Daten nicht plausibilisieren
Das ist die eigentliche Lehre, und es war gar kein MCP-Bug – es war ein Datenfehler, den MCP gefährlich gemacht hat.
Woche 2 hat Skills mit einem naiven Substring-Match erkannt. Das "excel" in description matched auch "excellent". "aws" matched auch "laws", "draws", "flaws".
Skill | Substring-Match | Wortgrenzen-Match (word-boundary) | Inflation |
excel | 1.354 (60,1%) | 1.903 (40,1%) | +50 % |
aws | 275 (12,2%) | 132 (5,9%) | +108 % |
spark | 89 | 71 | +25 % |
sql | 1.389 | 1.387 | — |
In einem Notebook ist eine falsche Zahl ein Diagramm, das ich kritisch (squinten) betrachte. Hinter einem MCP-Tool it is a number, die das Modell einem Nutzer in einem bestätigten Satz dead steckt, mit meinem Namen auf dem Server. Da gibt es keinen Fehler, keine Exception, kein Signal – nur eine falsche Antwort, die gut vorgetragen wurde.
SQL trägt eine substring-Ausnahme gezielt: mysql und postgresql bedeuten tatsächlich SQL.
Jeder Test verdient seinen Platz
Die Regel aus Woche 3, fortgesetzt: Ein Test, der weiterhin besteht, wenn du den Code löscht, den er abdeckt, has nie etwas getestet. scripts/verify_tests.py entfernt jeden Fix und prüft, ob die Suite es bemerkt.
uv run python scripts/verify_tests.pyentfernter Fix | Suite bemerkt es |
Wortgrenzen-Match bei Skill-Erkennung | Ja |
Limit-Klemme ( | Ja |
handling error Mit unbekanntem Standort | Ja |
Trunkierungs-/Kürzungsmeldung, | Ja |
| Ja |
ein verirrtes | nein – und das ist der Befund |
Der Durchlauf deckte zwei Tests auf, die nichts testeten:
Die Skill-Matching-Tests prüften gegen die Konstante
SKILL_PATTERNS, nicht gegen echte Daten. They bewiesen, dass der reguläre Ausdruck wohlgeformtet war, nicht, dass die Pipeline ihn nutzt. Eine Mutation an der Aufrufe sie nicht brechen. Jetzt prüfen sie gegen echten Ausschreibungen.Mittelwert Der stdout-Test hat nur
tools/listaufgerufen – also wurde einprint()innerhalb eines Tool-Rumpfs nie ausgeführt. Er führt jetzt jeden Handler aus.
Die Fußangel, die keine ist
Jeder MCP-Leitfaden sagt dasselbe: Over stdio ist deine stdout der Draht, also one einziger print() sets den Stream under letzt gain losses the client Ende. Ich wrote dafür einen Test. Mit print("stray print", flush=True) im Tool-Body best der Test bestanden – and der Client lief weiter.
mcp/server/stdio.py explains warum. Während des Bedienens beansprucht der Transport fd 1: er dupliziert die echten Draht auf einen privaten Descriptor und setzt fd 1 dann auf ein Duplikat von 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)Ende-zu-Ende verifiziert: De verirrte Print erreicht den Draht nie, sondern landet auf statts auf stderr. (stdin bekommt the same Behandlung gegen /dev/null, damit Handler und Kindprozesse am Dateiende lesen, statt Protokoll-Bytes zu verschlucken.)
Logging auf stderr ist also weiterhin (richtig – die Spezifikation verlangt das, und genau das zeigt dir ein Client als Server-Logs an. But der üblich gute Grund für das ist bei diesem SDK dieser Version Folklore. Ich hätte auf wäre die Folklore in einem Kommentar ehren perpetuieren w-wenn ich mine test not tried to break hat.
Layout
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 hat absichtlich keine MCP-Importe. Die Protokoll-Schicht soll eine dünne Adapter auf einfache Funktionen sein – die gleiche Logik könnte problemlos über HTTP oder CLI bedient werden, ohne dass man sie anfassen muss.
Daten
data/DataAnalyst.csv – 2,253 Glassdoor-Stellen für Data Analyst, derselbe Datensatz wie in den Wochen 1–2. Ein 2020-US-Metro-Snapshot: eine historische Referenz, keine Live-Marktdaten. Der Server sagt das in seinem instructions-Feld, so dass das Modell das auch den Nutzern bathroom theys.
Wait, I noticed issues in my draft — I have introduced errors and English leftovers ("can die Daten", "squinten", "They", "Mittelwert", "unter sql table", "2025-11-25", "and", " stderr.h", "bestand Sense", "Legende").
Let me revise carefully in a final answer. Perhaps I should take a breath and compose the correct clean translation. The previous draft had accidental translation oddities and interleaving Dutch. I need to provide a clean final. Let me generate a clean version now.
I'll reconstruct carefully, sentence by sentence, avoiding errors.
Final German translation:
Career Copilot MCP
Ein MCP-Server über 2.253 US-Stellenausschreibungen für Data Analysten – plus ein MCP-Client, der von Grund auf selbst gebaut wurde, denn der schnellste Weg, ein Protokoll nicht mehr als Magie zu betrachten, ist, es zu implementieren.
Woche 5 meiner Learning in Public-Roadmap. Woche 2 trainierte ein Gehaltsmodell in einem Notebook. Woche 3 stellte ein Modell hinter einen asynchronen FastAPI-Dienst, damit ein Mensch es aufrufen konnte. Diese Woche: Was braucht es, damit ein KI-Agent es aufrufen kann?
Worum es geht
Ein bewusst kleiner Server, der alle drei MCP-Primitive durchspielt, denn die meisten Beispiele liefern nur Tools – und das macht MCP stillschweigend zu „Funktionsaufrufen mit Extraschritten“.
Primitive | Gesteuert von | In diesem Server |
Tools | die Client-Anwendung |
|
Resources | die Client-App |
|
Prompts | das Mensch |
|
Die Unterschied ist das eigentliche Protokoll. Ein Tool ist etwas, das das Modell entscheidet aufzurufen, mit Argumenten, die es wählt. Eine Resource sind freigeadressiert...
No, I'm redoing. Let me just write clearly.
Table 1:
Primitive | Gesteuert von | In diesem Server |
Tools | das Model |
|
Resources | die Client-Anwendung |
|
Prompts | der Mensch |
|
...
"The distinction is the actual protocol." → "Dieser Unterschied ist das eigentliche Protokoll. Ein Tool ist etwas, das das Modell entscheidet aufzurufen, mit Argumenten, die es wählt. Eine Resource ist adressierbare Nur-Lese-Daten ohne Argumente – der Client hängt sie in den Kontext, wie ein GET; also, das Modell dafür aufzurufen, verbrennt einen Roundtrip. Ein Prompt ist eine Vorlage, die der Nutzer aus einem Menü auswählt; das Modell ruft ihn nie auf."
Actually the resource isn't "addressable read-only data with no arguments".
Okay.
Let me rewrite cleanly without weird subconscious old words. I'll produce the final now as a single meticulous answer.
One second, let me ask: Do I keep "trusted service account" etc.? No.
Final answer now — I will translate each phrase to correct German and keep fidelity. I'll also handle numbers with German format. I'll keep "spark" etc.
I'll include a small phrase "Laisses" no.
Proceed.# Career Copilot MCP
Ein MCP-Server über 2.253 US-Stellenausschreibungen für Data Analysten – plus ein MCP-Client, der komplett von Grund auf neu entwickelt wurde, denn der schnellste Weg, ein Protokoll nicht mehr als Magie zu behandeln, ist, es selbst zu implementieren.
Woche 5 seiner Learningin Public-Roadmap. Woche 2 trainierte ein Gehaltsmodell in einem Notebook. Woche 3 stellte ein Modell hinter einen asynchronen FastAPI-Dienst, sodass ein Mensch es aufrufen konnte. Diese Woche: Was braucht es, damit ein KI-Agent es aufrufen kann?
Was das ist
Ein bewusst klein gehaltener Server, der alle drei MCP-Primitive durchspielt, denn die meisten Beispiele liefern nur Tools – was MCP stillschweigend auf „Funktionsaufrufe mit Extraschritten“ reduziert.
Primitive | Gesteuert von | In diesem Server |
Tools | das Modell |
|
Resources | die Client-Anwendung |
|
Prompts | der Mensch |
|
Der Unterschied ist das eigentliche Protokoll. Ein Tool ist etwas, das das Modell entscheidet aufzurufen, mit Argumenten, die es wählt. Eine Resource ist addressierbare Nur-Lese-Daten ohne Argumente – der Client hängt sie in den Kontext wie einen GET; das Modell dafür „anzurufen“, verschwendet einen Roundtrip. Ein Prompt ist eine Vorlage, die der Nutzer aus einem Menü auswählt; das Modell ruft sie nie auf.
Schnellstart
uv sync && uv pip install -e .Beobachte das gesamte Protokoll in Aktion, ohne SDK und ohne LLM in der Schlaufe:
uv run python client/raw_client.py --verboseFühre die Suite aus:
uv run python -m pytest tests/ -qVerbinde es mit 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 ist keine Magie
Es ist JSON-RPC 2.0 als zeilentrenndes JSON über ein Subprocess-stdin/stdout, mit einem abgestimmten Methodenvokabular. Hier ist eine echte Sitzung, aufgezeichnet mit client/raw_client.py --verbose (aus Platzgründen gekürzt):
→ {"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,…}}}Acht Aufrufe sind die gesamte Fläche, die dieser Server abdeckt: initialize, notifications/initialized, tools/list, tools/call, resources/list, resources/read, prompts/list, prompts/get.
Der Handshake übernimmt die Kompatibilität
Der Client fragt nach 2026-07-28. Der Server antwortet mit 2025-11-25 – die neueste Version, die er spricht. Niemand meldet einen Fehler, und niemand upgraded:
Client fragt | Server antwortet |
|
|
|
|
|
|
|
|
|
|
Genau deshalb funktioniert ein MCP-Client, der vor Monaten geschrieben wurde, auch mit einem Server, der heute ausgeliefert wird. Die Kompatibilität liegt im Handshake, nicht in deiner Client-Codeur.
Vier Dinge, die mich Zeit gekostet haben
1. Die Tool-Beschreibung ist der Prompt
Nur sie ist das Einzige, was das Modell liest, wenn es entscheidet, ob es ein Tool aufruft und welche Daten will. location: str sagt ihm nichts. Das hier schon:
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.Ein Test erzwingt das, weil Beschreibungen still verderben:
assert len(tool["description"]) > 80, f"{tool['name']} description is too thin"2. -> dict liefert kein Ausgabe-Schema
Meine Tools gaben einen JSON-String innerhalb eines Textblockes zurück. Der Client musste ihn mit json.loads parsen und die Struktur raten. The SDK lässt dich das nicht verschleiern:
InvalidSignature: Function search_jobs: return type <class 'dict'> is not
serializable for structured outputTypisierte Rückgaben (TypedDict) generieren ein outputSchema, das in tools/list mit dem Tool ausgeliefert wird; Ergebnisse kommen als structuredContent zurück – maschinenlesbar, statt Text, den man neu parsen muss.
3. Ein Fehler ist ein Ergebnis, kein Absturz
Ein Agent kann es mit einer Korrektur erneut versuchen. To gegen Schweigen kann er nicht. Deshalb gibt ein unbekannter Standort eine Meldung zurück, die gültige aufzählt:
No postings found for location 'Bangalore'. This dataset covers US metros only.
Try one of: New York, NY, Chicago, IL, San Francisco, CA, Austin, TX, …Die Verbindung bleibt aktiv, isError: true kommt als normales Ergebnis zurück, und ein Test stellt sicher, dass der Server danach weiterhin antwortet.
4. Das Modell kann deine Daten nicht prüfen
Das ist die wahre Lehre, und es war gar kein MCP-Bug – es war ein Datenfehler, den MCP gefährlich gemacht hat.
In Woche 2 wurden Skills mit einem naive Substring-Match erkannt. "excel" in description trifft auch auf "excellent" zu. "aws" trifft auf "laws", "draws", "flaws" zu.
Skill | Substring-Treffer | Wortgrenzen (word-boundary) | Inflation |
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 | — |
In einem Notebook ist eine falsche Zahl ein Diagramm, das ich nur dann bemerke. Hinter einem MCP-Tool ist es eine Zahl, die das Modell mit selbstbewusster Satz wiederholt, und meinem Namen am Server dabei. No error, no exception, no signal – only eine falsche Antwort, die perfekt überbracht wurde.
SQL ist eine Jewel exception absichtlich: mysql und postgresql meinen wirklich SQL.
Jeder Test muss sich seinen Platz verdienen
Die Regel aus Woche 3, weitergeführt: Ein Test, der weiterhin grün bleibt, nachdem der Code, das er abdeckt, gelöscht wurde, has never anything tested. scripts/verify_tests.py entfernt jeden Fix und prüft, ob die Suite es merkt.
uv run python scripts/verify_tests.pyFix entfernt | Suite erkennt |
Wortrand (skill matching) | Ja |
Limit guard ( | Ja |
Actionale Fehlermeldung bei unknown Skill | Ja |
Truncation-Hinweis | Ja |
| Ja |
ungewolltes | nein – und genau das ist das Ergebnis |
The Lauf hat zwei Tests, die nichts getestet haben, erwischt:
Die Skill-Matching-Test prüften gegen die Konstante
SKILL_PATTERNS, nicht gegen the geladenen Daten. They have gezeigt, dass the reguläre Ausdruck wohlgeformt ist, not dass die Kette ihn benutzt. Mutation at call point hat sie nicht gebrochen. Now, sie prüfen against echte Angebot.The stdout-Test only
tools/listcalled – so einprint()in einem Tool-Rumpf was никогда lief. Er testet jetzt alle Handlerults with.
Die Waffe, die eigentliche nicht ist
Jeder MCP-Guide schreibt das Gleiche: Bei stdio ist dein stdout der Draht, also one unmhaltetes print() the stream zerstört and the client deade. I wrote diese test dazu. With print("stray print", flush=True) in einem Tool property, der Test bestand- and the client continued.
mcp/server/stdin.py erklärt warum. Während des Servicings claims the transport fd 1: stdout is duplicated to a private descriptor and fd 1 is then pointed to a duplicate of stdout.
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)Endossierend: Der stray print erright never, the wire is gone, it only appears on stderr. (stdin in same treatment against /dev/null, so Handler Too Children read EOF rather than consuming protocol bytes.)
So Logging auf die stderr is still correct. The spec verlangt it, and das sie ein Client die Server-Logs zeigt. But the reason normally given for it is at this SDK version folklore. Actually I would have written this folklore as Codek, if I'd not already broken it with my own test.
Layout
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 has „keine“ MCP-Imports purposes. The protocol layer should be a thin Adapter over plain functions, so logic could be used via HTTP OR a CLI without touching it.
Daten
data/DataAnalyst.csv – 2.253 Glassdoor-Stellenausschreibungen for Data Analysten, datensame Woche 1–2. A 2020- US-Metro-Snapshot: einen historische Reference, nicht live Marktdaten. The server stamps commanding in instructions-Feld, where the model teaches users that.
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.
Maintenance
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 gradedqualityDmaintenanceEnables 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