job-search-mcp
by JonniThorpe
README.md
# job-search-mcp
Servidor MCP local que gestiona una busqueda de empleo sobre archivos Markdown.
El servidor no razona. El usuario habla con Claude, y Claude usa este servidor
para leer y escribir sus archivos con una estructura fija. **El cerebro es el
cliente; esto es la memoria y las manos.**
De un solo usuario, sin red, sin base de datos.
---
## Concepto central: dos archivos separados
Es la idea que sostiene todo lo demas.
- **`perfil-skills.md`** es lo que YA se tiene. Todo a nivel `[solido]` o
`[parcial]`, nunca huecos. Un termino entra aqui solo cuando el usuario
confirma que lo tiene.
- **`radar-huecos.md`** es lo que el mercado pide y no se tiene, cada termino con
un **contador** de cuantas ofertas objetivo lo han pedido. Ordenado por
contador: lo de arriba es lo mas urgente de aprender.
Ciclo de vida de una skill: nace como hueco cuando una oferta la pide, su
contador sube cada vez que otra oferta la vuelve a pedir, y **se gradua** cuando
el usuario confirma que la ha adquirido: sale del radar y entra en el perfil con
su nivel. Ese es el unico camino de hueco a perfil.
---
## Modelo de datos
Todo vive en `DATA_DIR`, un directorio que es a la vez una boveda de Obsidian.
El MCP escribe los `.md` y Obsidian los renderiza: la misma carpeta, cero
conflicto.
```
DATA_DIR/
perfil/
perfil-skills.md - [solido|parcial] Termino (nota) bajo '## Categoria'
radar-huecos.md - [contador: N] Termino (Categoria). Fuentes: a; b
criterios.md prosa libre: que hace que una oferta merezca el tiempo
cv.md CV en Markdown, fuente de la evidencia concreta
ofertas/
AAAA-MM-empresa.md frontmatter YAML + analisis + registro de estados + notas
```
Estados de una oferta:
`analizada -> solicitada -> entrevista -> oferta -> aceptada`, con dos salidas
laterales desde cualquier punto: `denegada` y `descartada`.
### El servidor nunca regenera un archivo
Parsea solo las lineas que reconoce y edita **esa linea concreta**, dejando el
resto de bytes intactos. El discriminador es el patron (`- [contador: N] `), no
la posicion: por eso una seccion `## Reglas` llena de vinietas normales convive
sin problema con las lineas de datos.
Esto es deliberado. Los archivos los edita tambien un humano en Obsidian, asi que
**el formato es del usuario, no del servidor**.
---
## Superficie MCP
### Resources (solo lectura, los adjunta el usuario)
| URI | Contenido |
| --- | --- |
| `profile://skills` | perfil-skills.md |
| `profile://gaps` | radar-huecos.md, ordenado por contador al servir |
| `profile://cv` | cv.md |
| `profile://criteria` | criterios.md |
| `offers://list` | tabla del pipeline: estado, empresa, puesto, score |
| `offers://{id}` | una oferta entera: analisis, estados y notas |
### Tools (escriben, los invoca el modelo)
| Tool | Que hace |
| --- | --- |
| `record_gap(term, category, source)` | Sube el contador en 1 y anade la fuente. Si no existe, lo crea con 1. Nunca duplica. |
| `list_gaps()` | Huecos ordenados por contador descendente. |
| `confirm_skill(term, level, evidence, category?)` | La graduacion: saca del radar y mete en el perfil. Descarta las fuentes: al perfil solo viaja la evidencia. |
| `update_skill_level(term, level)` | Cambia solido/parcial de algo que ya esta en el perfil. |
| `save_offer(id, title, company, markdown, score?, salario?, url?, tags?)` | Crea o actualiza una oferta. Al actualizar conserva estado y notas. |
| `set_offer_status(id, status)` | Mueve por el ciclo de vida y lo anota con fecha. |
| `add_offer_note(id, note)` | Nota fechada en "Notas de entrevista". |
| `ping()` | Diagnostico: responde y dice sobre que `DATA_DIR` trabaja. |
### Prompt (lo invoca el usuario)
`analyze_offer` devuelve la plantilla de analisis rellenada con perfil, CV y
criterios **leidos del disco en ese instante**, mas las instrucciones de guardar
la oferta con `save_offer` y registrar cada hueco con `record_gap`. Debajo se
pega el texto de la oferta.
---
## Instalacion
Requiere Node >= 18 (probado con 24).
```powershell
npm install
npm run build
```
Configuracion: una unica variable de entorno.
| Variable | Por defecto |
| --- | --- |
| `JOB_MCP_DATA_DIR` | `~/Desktop/Productivo/Trabajo/Perfil` |
Comprobar que arranca (imprime la ruta de datos y se queda esperando en stdio):
```powershell
$env:JOB_MCP_DATA_DIR="<ruta a la boveda>"
node build/index.js
```
### Registrarlo en un cliente
Es un servidor **stdio**: el cliente lo lanza como proceso hijo. La declaracion
tiene siempre esta forma:
```jsonc
{
"command": "node",
"args": ["<ruta al repo>/build/index.js"],
"env": { "JOB_MCP_DATA_DIR": "<ruta a la boveda>" }
}
```
- **Claude Code**: `~/.claude.json`, o `claude mcp add job-search --scope user -- node <ruta>/build/index.js`.
- **Claude Desktop**: bloque `mcpServers` en `claude_desktop_config.json`. Usa la
ruta **absoluta** del ejecutable de node: Desktop se lanza desde el explorador
y no hereda el PATH.
> Las rutas concretas de esta maquina estan en `SETUP.local.md`, fuera de git.
---
## La regla que mas tiempo cuesta aprender
> **Cada `npm run build` obliga a reiniciar el cliente.**
Un servidor MCP por stdio se lanza **una vez**, cuando arranca el cliente, y ese
proceso se queda vivo. No hay recarga en caliente. Cerrar la ventana de Claude
Desktop no basta: hay que salir desde el icono de la bandeja del sistema.
Sintoma tipico: pides un tool nuevo y el cliente jura que no existe.
---
## Desarrollo
```powershell
npm run build # tsc -> build/
npm test # tsc + node --test "build/**/*.test.js" (40 tests)
npm run watch # recompilar al guardar
```
```
src/
index.ts solo cablea: resources + tools + prompts + transporte stdio
config.ts DATA_DIR y rutas derivadas
resources.ts los 6 resources
tools.ts los 8 tools, con sus esquemas Zod
prompts.ts analyze_offer
lib/
markdown.ts primitivas por linea: cabeceras, secciones, EOL, normalize
skills.ts perfil-skills.md
radar.ts radar-huecos.md y los contadores
offers.ts frontmatter, estados y notas
graduate.ts confirm_skill: la operacion que cruza los dos archivos
files.ts el UNICO modulo con I/O
```
`src/lib/*` son **funciones puras** con la forma
`(contenido, args) -> { contenido, changed, message }`. Por eso los tests no
tocan el disco y corren en 100 ms.
`graduate.ts` devuelve los dos contenidos nuevos sin escribir: quien llama
persiste ambos o ninguno. Es lo mas cerca de una transaccion que tiene sentido
aqui.
**Nunca uses `console.log` en el servidor.** stdout es el canal JSON-RPC y
escribir ahi rompe el protocolo. Los logs van a `console.error` (stderr).
Probar el servidor sin ningun cliente, hablandole JSON-RPC crudo:
```powershell
$msgs = @(
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"m","version":"0"}}}'
'{"jsonrpc":"2.0","method":"notifications/initialized"}'
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
)
$msgs | node build/index.js
```
---
## Donde aparece cada primitivo en el cliente
Los tres primitivos tienen puntos de entrada distintos, segun quien decide usarlos:
| Primitivo | Quien lo invoca | Donde aparece |
| --- | --- | --- |
| Prompt | el usuario | menu `/` |
| Tool | el modelo | no se invoca desde un menu: se pide hablando |
| Resource | el usuario | menu de adjuntar |
---
## Obsidian
El `DATA_DIR` es una boveda. Dos plugins, y de momento solo estos:
- **Dataview**: tabla viva del radar ordenada por contador, y tabla de ofertas
filtrada por estado, apoyada en el frontmatter YAML.
- **Kanban**: el pipeline de ofertas como tablero por estado.
La vista de grafo y los `[[enlaces]]` salen gratis.
---
## Fuera de alcance
Sin base de datos (son Markdown a proposito, legibles y versionables a mano), sin
interfaz web, sin scraping de portales, sin multiusuario, sin nube. Corre local.
Las mejoras aparcadas estan en `IDEAS.md`.
TDQS
A4.4/5.0
Scored across 8 tools
Disambiguation5/5
Each tool has a distinct purpose: skill management (update_skill_level, record_gap, list_gaps, confirm_skill), offer management (save_offer, set_offer_status, add_offer_note), and health check (ping). No overlapping or ambiguous tools.
Naming Consistency5/5
All tools follow a consistent verb_noun pattern in snake_case, e.g., record_gap, list_gaps, set_offer_status. The naming is predictable and clear.
Tool Count5/5
With 8 tools covering skill tracking and offer management, the server is well-scoped. Each tool serves a necessary function without redundant or excessive entries.
Completeness4/5
The set covers core workflows: skill gap analysis and offer lifecycle. Missing features like listing offers or direct skill removal are minor gaps that agents can work around.
Maintenance
ActivitySlowing
ResponsivenessNo issues