Career Copilot MCP
Career Copilot MCP
Un servidor MCP sobre 2,253 ofertas de trabajo de analista de datos en EE. UU. — y con un cliente MCP hecho desde cero, porque la forma más rápida de dejar de tratar un protocolo como magia es implementarlo.
Semana 5 de mi hoja de ruta Learning in Public. La Semana 2 entrenó un modelo salarial en un notebook. La Semana 3 puso un modelo detrás de un servicio FastAPI asíncrono para que una persona pudiera llamarlo. Esta semana: ¿qué hace falta para que un agente de IA lo llame?
Qué es esto
Un servidor deliberadamente pequeño que ejercita las tres primitivas de MCP, porque la mayoría de los ejemplos solo incluyen tools, lo que, silenciosamente, reduce MCP a "llamada a funciones con pasos extra".
Primitiva | Controlado por | En este servidor |
Herramientas | el modelo |
|
Recursos | la aplicación cliente |
|
Prompts | el humano |
|
La distinción es el protocolo real. Una herramienta es algo que el modelo decide llamar, con argumentos que él mismo elige. Un recurso son datos direccionables de solo lectura, sin argumentos: el cliente lo adjunta al contexto como un GET, así que obligar al modelo a "llamarlo" desperdicia una ida y vuelta. Un prompt es una plantilla que el usuario selecciona de un menú; el modelo nunca lo invoca.
Inicio rápido
uv sync && uv pip install -e .Observa cómo se ejecuta el protocolo entero, sin SDK y sin LLM en el bucle:
uv run python client/raw_client.py --verboseEjecuta la suite:
uv run python -m pytest tests/ -qConéctalo a 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 no es magia
Es JSON-RPC 2.0 como JSON delimitado por saltos de línea sobre la stdin/stdout de un subproceso, con un vocabulario de métodos acordado. Esta es una sesión real, capturada de client/raw_client.py --verbose (truncada para ancho):
→ {"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,…}}}Ocho llamadas es toda la superficie que usa este servidor: initialize, notifications/initialized, tools/list, tools/call, resources/list, resources/read, prompts/list, prompts/get.
El apretón de manos hace el trabajo de compatibilidad
El cliente pide 2026-07-28. El servidor responde 2025-11-25 — la versión más reciente que él habla. Nadie da un error y nadie se actualiza:
el cliente pide | respuesta del servidor |
|
|
|
|
|
|
|
|
|
|
Por eso un cliente MCP escrito hace meses sigue funcionando hoy con un servidor publicado hoy. La compatibilidad está en el apretón de manos, no en tu código.
Cinco cosas que me costaron tiempo
1. La descripción de la tool es el prompt
Es lo único que el modelo lee cuando decide si llamar a una herramienta y qué pasarle. location: str no le dice nada. Esto sí:
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.Un test lo verifica, porque las descripciones se vuelven obsoletas y nadie detecta de qué modo:
assert len(tool["description"]) > 80, f"{tool['name']} description is too thin"2. -> dict no te da un esquema de salida
Mis tools devolvían un string JSON dentro de un bloque de texto. El cliente tenía que json.loads y adivinar la forma. El SDK no te deja tapar esto con azúcar:
InvalidSignature: Function search_jobs: return type <class 'dict'> is not
serializable for structured outputLos valores de retorno tipados (TypedDict) generan un outputSchema que viaja con la tool en tools/list, y los resultados regresan en structuredContent — legible para máquinas, no texto para reparsear.
3. Un error es un resultado, no un crash
Un agente puede reintentar la operación con una sugerencia. No puede reintentar contra el silencio. Así, una ubicación desconocida devuelve un mensaje que nombra ubicaciones válidas:
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, …La conexión sigue activa, isError: true regresa como resultado normal, y un test prueba que el servidor sigue respondiendo después.
4. El modelo no puede validar tus datos
Esta es la lección real, y no era un bug de MCP en absoluto — era un bug de datos que MCP volvía peligroso.
La Semana 2 detectaba habilidades con una coincidencia ingenua de substring. "excel" in descripcion también coincide con "excellent". "aws" coincide con "laws", "draws", "flaws".
habilidad | coincidencia por substring | coincidencia por palabra | inflación |
excel | 1,354 (60.1%) | 903 (40.4%) | +50% |
aws | 275 (12.2%) | 132 (5.9%) | +108% |
spark | 89 | 71 | +25% |
sql | 1,389 | 1,387 | — |
En un notebook, un número equivocado es un gráfico que miro con los ojos bizcos. En una MCP tool, un número equivocado es algo que el modelo le repite al usuario en una frase con total confianza, con mi nombre en el servidor. No hay error, no hay excepción, no hay señal; solo una respuesta incorrecta entregada con buenos modales.
SQL mantiene esa excepción de substring adrede: mysql y postgresql realmente significansign SQL.
Cada test se gana su lugar
La regla de la Semana 3 se ha aplicado aquí: un test que aún pasa cuando eliminas el código que cubre no estaba probando nada en realidad. scripts/verify_tests.py elimina cada corrección y comprueba que la suite se da cuenta.
uv run python scripts/verify_tests.pycorrección eliminada | ¿la suite sospecha? |
coincidencia de habilidades con límite de palabra | sí a reyes |
límite ( | sí |
error de ubicación desconocida con sugerencia | sí |
informe de truncamiento | sí |
anotaciones | sí |
un | no — y ese es el hallazgo |
Al ejecutarlo se detectaron dos tests que no probaban nada:
Los tests de habilidades verificaban la constante
SKILL_PATTERNS, no los datos reales cargados. Demostraban que la regex estaba bien formada, no que el pipeline la usara. Alterar el punto de llamada no los rompía. Ahora apuntan contra ofertas reales.El test de stdout solo llamaba a
tools/list, así que unprint()dentro del cuerpo de una tool nunca se ejecutaba. Ahora ejecuta todos los handlers.
La trampa que no existe
Todos los tutoriales de MCP dicen lo mismo: con stdio, tu stdout es el cable, así que un print() suelto corrompe el flujo y mata al cliente. Escribí un test para ello. Con print("stray print", flush=True) añadido al cuerpo de una tool, el test pasó — y el cliente siguió funcionando.
mcp/server/stdio.py explica por qué. Mientras sirve, el transporte se apodera del fd 1: duplica el cable real a un descriptor privado, luego apunta fd 1 a un duplicado de 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)Comprobado en upstream completo: el print() suelto no llega nunca al protocolo y acaba en el stderr. (stdin recibe el mismo tratamiento contra /dev/null, así que los handlers y subprocesos leen EOF en vez de comerse los bytes del protocolo.)
Por eso registrar en stderr sigue siendo correcto — la especificación lo pide y es lo que el cliente muestra como logs del server. Pero la razón que se suele dar para ello, en esta versión del SDK, es folclor. Habría incluido ese folclor y un comentario si no hubiera intentado romper mi propio test.
Estructura
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 no tiene imports de MCP de manera intencionada. La capa de protocolo debe ser un adaptador fino sobre funciones planas: la misma lógica podría servirse sobre HTTP o una CLI sin tener que tocarla.
Datos
data/DataAnalyst.csv — 2,253 ofertas de empleoce-analista de datos de Glassdoor, el mismo dataset de la Semanas 1-12. Una foto instantánea de 2020 en Estados Unidos: una referencia histórica, no datos de mercado en vivo. El server lo indica en su campo instructions, para que el modelo también se lo diga a los usuarios.
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