Skip to main content
Glama
Aniruddha-Shukla

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.

Python MCP Tests

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

search_jobs, salary_benchmark, skill_demand

Recursos

la aplicación cliente

market://snapshot, market://locations

Prompts

el humano

career_gap_review

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 --verbose

Ejecuta la suite:

uv run python -m pytest tests/ -q

Coné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

2026-07-28 (más nueva que el servidor)

2025-11-25

2025-11-25

2025-11-25

2025-06-18

2025-06-18

2024-11-05

2024-11-05

1999-01-01 (sin sentido)

2025-11-25

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 output

Los 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.py

corrección eliminada

¿la suite sospecha?

coincidencia de habilidades con límite de palabra

sí a reyes

límite (1 ≤ limit ≤ 25)

error de ubicación desconocida con sugerencia

informe de truncamiento

anotaciones readOnlyHint

un print() suelto en el cuerpo de una tool

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 un print() 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 it

market.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.

-
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