Skip to main content
Glama
timur-ai

ati-hh-mcp

by timur-ai

ati-hh-mcp

CI Docs Python Coverage License: MIT

Public MCP (Model Context Protocol) server for the hh.ru API: every operation of the public hh.ru OpenAPI specification – all 135 of them – exposed as grouped, switchable toolsets for LLM agents. stdio for personal use, streamable HTTP with API keys and OAuth for teams.

  • Complete and verified: 135/135 public OpenAPI operations are implemented and locked by a contract metatest (operations and required parameters); responses are checked against the vendored specification with live-recorded fixtures.

  • Safe by design: mutations require an explicit confirm=true argument; GET requests are throttled and retried on 429, mutations never repeat automatically; tokens never appear in logs.

  • Built for LLMs: domain-prefixed snake_case tool names, concise English descriptions, structured errors with hh.ru codes and Russian descriptions.

Русская версия документации

Contents

Related MCP server: HeadHunter API MCP Server

Requirements

  • Python 3.10 – 3.14 (CI matrix covers every minor version), or Docker.

  • uv for the uvx/stdio path.

  • An hh.ru API token or a registered OAuth application for authorized calls; public dictionaries, suggestions and vacancy search work without a token.

Quick start

uvx from git (stdio)

No clone, no install – the server runs straight from the repository:

uvx --from git+https://github.com/timur-ai/ati-hh-mcp ati-hh-mcp serve --transport stdio

The command starts the server on stdio with the default toolsets. Point an MCP client at it (see client configuration) and set HH_TOKEN in the server environment for authorized tools.

Docker Compose (HTTP)

git clone https://github.com/timur-ai/ati-hh-mcp
cd ati-hh-mcp
MCP_API_KEYS="$(openssl rand -hex 32)" docker compose up --build

Clients connect to http://localhost:8080/mcp with Authorization: Bearer <your-api-key>. The container is non-root, has a /health endpoint for the Compose healthcheck, and keeps OAuth tokens in a SQLite volume. See the deployment guide for multi-user setup.

TLS is mandatory for network exposure: the server speaks plain HTTP, so put a TLS-terminating reverse proxy (Caddy/nginx) in front of it, bind the container to 127.0.0.1 and run a single worker process – see the deployment guide for ready-made proxy snippets.

Client configuration

ZCode (stdio)

.zcode/settings.json in the project (or the user config):

{
  "mcpServers": {
    "hh": {
      "command": "uvx",
      "args": [
        "--from", "git+https://github.com/timur-ai/ati-hh-mcp",
        "ati-hh-mcp", "serve", "--transport", "stdio"
      ],
      "env": {
        "HH_TOKEN": "YOUR_HH_TOKEN",
        "HH_UA_EMAIL": "you@example.com"
      }
    }
  }
}

Claude Desktop (stdio)

claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/claude_desktop_config.json, Windows: %APPDATA%\Claude\claude_desktop_config.json):

{
  "mcpServers": {
    "hh": {
      "command": "uvx",
      "args": [
        "--from", "git+https://github.com/timur-ai/ati-hh-mcp",
        "ati-hh-mcp", "serve", "--transport", "stdio"
      ],
      "env": {
        "HH_TOKEN": "YOUR_HH_TOKEN",
        "HH_UA_EMAIL": "you@example.com"
      }
    }
  }
}

Cursor (stdio and HTTP)

.cursor/mcp.json. stdio launch:

{
  "mcpServers": {
    "hh": {
      "command": "uvx",
      "args": [
        "--from", "git+https://github.com/timur-ai/ati-hh-mcp",
        "ati-hh-mcp", "serve", "--transport", "stdio"
      ],
      "env": {
        "HH_TOKEN": "YOUR_HH_TOKEN",
        "HH_UA_EMAIL": "you@example.com"
      }
    }
  }
}

Connection to the Docker/HTTP deployment:

{
  "mcpServers": {
    "hh": {
      "url": "http://localhost:8080/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_MCP_API_KEY"
      }
    }
  }
}

Toolsets

62 tools are enabled by default; six employer toolsets and the undocumented extended set are opt-in via HH_TOOLSETS. HH_TOOLSETS fully replaces the default list; the auth toolset is always included regardless. A generated catalog with every tool, its OpenAPI operation and description lives in the tool reference.

Toolset

Tools

Default

Domain

auth

5

always on

/me profile, OAuth login/exchange/status/logout

dictionaries

16

on

areas, industries, professional roles, languages, locales, metro, skills, universities, vacancy conditions

suggests

11

on

search suggestions: areas, companies, positions, skills, keywords

vacancies

4

on

vacancy search, similar and related vacancies

negotiations

10

on

responses/invitations: lists, states, messages, tests, invitations

chats

14

on

chats: messages, participants, files, read counters

resumes

2

on

resume viewing, call-tracking phone numbers

employer_vacancies

23

off

employer vacancy CRUD, drafts, publication, statistics

employer_negotiations

5

off

employer responses/invitations, mail templates, statistics

employer_resumes

11

off

resume search (52 parameters), saved searches, applicant comments

employer_managers

11

off

managers, settings, access and limits

employer_misc

25

off

employers info, addresses, departments, calls, webhooks, salary statistics

extended

4

off

undocumented: apply to a vacancy, own resumes, artifacts

Enable extra toolsets with a comma-separated list, for example HH_TOOLSETS=employer_vacancies,employer_resumes.

Authorization

The server separates access to the MCP server from access to hh.ru:

  • Authorization: Bearer <key> (HTTP mode) – the MCP API key from MCP_API_KEYS; protects the server itself in multi-user deployments.

  • X-HH-Token: <hh-token> – the hh.ru token for a single request (HTTP mode).

  • OAuth profile – token pairs stored in SQLite per MCP API key.

An hh.ru token can be provided in three ways.

1. Static token

Set HH_TOKEN in the server environment (the usual stdio path). Applies to every call of the deployment; in HTTP mode with MCP_API_KEYS set, HH_TOKEN is deliberately ignored to avoid mixing user profiles.

2. Per-request token header

Send X-HH-Token: <token> with an MCP call (HTTP mode). The token is used statelessly for that request only and is never stored.

3. OAuth login

With HH_CLIENT_ID, HH_CLIENT_SECRET and HH_REDIRECT_URI configured:

  • stdio: call auth_get_login_url, open the URL in a browser, log in on hh.ru, copy the code parameter from the redirect address, then call auth_exchange_code(code). Token pairs land in the local SQLite store and refresh automatically.

  • HTTP: same flow, but the redirect goes to GET /oauth/callback on the server and the exchange completes automatically. The OAuth state parameter is bound to the caller's profile, so tokens land in the right SQLite profile.

auth_status shows the current profile state, auth_logout calls DELETE /token on hh.ru and drops the stored pair.

Registering an hh.ru application

  1. Sign in at dev.hh.ru and open the application admin.

  2. Create an application; you will get HH_CLIENT_ID and HH_CLIENT_SECRET.

  3. Register the redirect URI:

    • HTTP deployment: http://<your-host>:8080/oauth/callback (for local testing http://localhost:8080/oauth/callback);

    • stdio: any previously registered URI works – you copy the code manually.

  4. Set HH_CLIENT_ID, HH_CLIENT_SECRET, HH_REDIRECT_URI in the server environment and restart.

Two-level search schema

Heavy search operations (search_vacancies with 45 parameters, employer_search_resumes with 52) would bury an LLM in flat arguments. Such tools use a two-level schema: frequent filters are top-level tool parameters, the long tail lives in a nested advanced object:

{
  "text": "python developer",
  "area": "1",
  "salary": 200000,
  "advanced": { "work_format": "remote", "date_from": "2026-01-01" }
}

Unset fields – top-level or nested – are simply not sent to hh.ru. The allowed subset is locked in the tool registry and verified by the contract metatest, so no parameter outside the OpenAPI operation can sneak in.

Architecture

flowchart LR
    A["MCP client<br/>(ZCode, Claude, Cursor)"] -- "stdio" --> S
    A -- "HTTP + Bearer MCP API key" --> S
    S["FastMCP server<br/>transports - auth middleware - /health - /oauth/callback"]
    S --> R["Registry<br/>tool = operation (135/135)"]
    S --> T["13 toolsets / 141 tools<br/>mutating tools require confirm"]
    T --> K["toolkit make_tool<br/>path/query/body packing"]
    K --> C["HHClient<br/>User-Agent - token bucket - 429 retries (GET only)"]
    C -- "HTTPS + Bearer hh.ru token" --> HH["api.hh.ru"]
    S -.-> DB[("TokenStore SQLite<br/>OAuth profiles per API key")]
    B["browser OAuth"] -.-> S
  • Transports: stdio for personal launch (uvx git+...), streamable HTTP with MCP_API_KEYS for teams; /health for Docker healthchecks.

  • Registry: the single source of truth mapping every tool to its OpenAPI operation; a contract metatest enforces required params of the operation subset of tool params subset of operation params.

  • HHClient: token-bucket throttling (default 25 req/min), the User-Agent contact policy of hh.ru, automatic 429 retries for GET only – mutations fail loudly instead of repeating.

  • TokenStore: SQLite, one OAuth profile per MCP API key (keyed by the sha256 hash – raw keys are never stored); automatic refresh on expiry.

Configuration

Environment variable

Default

Purpose

HH_TOKEN

–

hh.ru token for all calls (stdio path; ignored in HTTP mode with MCP_API_KEYS)

HH_CLIENT_ID

–

OAuth application client id (dev.hh.ru)

HH_CLIENT_SECRET

–

OAuth application client secret

HH_REDIRECT_URI

–

OAuth redirect URI registered at dev.hh.ru

HH_TOOLSETS

default set

Comma-separated toolsets to enable; fully replaces the default list (auth is always included)

HH_API_BASE

https://api.hh.ru

API base URL (tests and experiments)

HH_UA_EMAIL

placeholder

Contact email in the User-Agent (hh.ru requirement)

HH_RATE_LIMIT

25

Requests per minute (token bucket)

HH_TOKEN_STORE

platform dir

SQLite path for OAuth token pairs (Docker: /data/tokens.sqlite3)

MCP_API_KEYS

–

Comma-separated API keys guarding the HTTP endpoint (case-sensitive)

Limitations

Stated honestly:

  • No resume CRUD. The public hh.ru OpenAPI has no operations to create or edit resumes; the resumes toolset is read-only by design.

  • The extended toolset is undocumented API. apply_to_vacancy, my_resumes, artifacts_list, artifact_delete rely on endpoints outside the public specification; they are disabled by default, marked [UNDOCUMENTED hh.ru API] and may break without notice.

  • Authorization header semantics. Authorization carries the MCP server API key, not the hh.ru token; the hh.ru token travels in X-HH-Token or HH_TOKEN. This mirrors the two-tenant reality (server access vs hh.ru access) and is an intentional deviation from the hh.ru API convention.

  • Retries are GET-only. A timed-out mutation is reported as an error with a "may have been applied" hint; it is never retried automatically.

  • The live API drifts from the specification. Responses are validated against the vendored OpenAPI with live-recorded fixtures; the single confirmed deviation (GET /suggests/companies returns null in non-nullable url, area.name, logo_urls) is pinned in KNOWN_DEVIATIONS of tests/contract/test_schemas.py.

  • Search endpoints are geo-blocked for datacenter IPs by hh.ru; the fixture recorder skips them (403 forbidden) when run from such networks. The server itself is unaffected when run from an allowed network.

Documented deviations

From the original design decisions, recorded with reasons:

  • FastMCP 4.1 instead of "FastMCP 2": the plan fixed the family, the concrete major was chosen in the M1 spike by stability and Python 3.10–3.14 support criteria; pinned in uv.lock. Details: ADR Z1.

  • __main__ coverage floor is 90 %, below the project-wide 95 % – the CLI entrypoint is a thin argument parser around tested components.

  • Distribution is sources-only (git + Docker): no PyPI, no GHCR, per the project decision.

Development

uv sync --dev                # environment
uv run pytest -q             # 547 tests (+8 live, opt-in): unit + contract + MCP e2e
uv run ruff check src tests  # lint
uv run mypy src              # strict type check

Useful maintenance scripts:

uv run python scripts/record_fixtures.py    # re-record live response fixtures (read-only, sanitized)
uv run python scripts/gen_tool_docs.py      # regenerate docs/tools/ from the registry
bash scripts/fetch_openapi.sh               # refresh the vendored OpenAPI spec
uv run mkdocs serve                         # documentation site locally

Live integration tests (tests/integration_live/, marker live) are opt-in and safe: read-only calls, token from the HH_LIVE_TOKEN secret, masked in logs. The documentation site builds strictly (mkdocs build --strict) and deploys to GitHub Pages from main on every push.

License

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables AI assistants to search job vacancies, manage resumes, and apply to jobs on HeadHunter (hh.ru), Russia's largest job search platform. Includes OAuth 2.0 integration for secure job applications and an automated vacancy hunter agent with intelligent matching.
    32
    MIT
  • A
    license
    Not graded
    quality
    F
    maintenance
    Enables AI assistants to access and manage HeadHunter job platform data, including vacancies, resumes, negotiations, and employer settings via 167+ tools.
    41 npm
    5
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Exposes iiko restaurant management system REST API as 42+ LLM-callable tools over stdio, enabling natural language interaction with products, employees, orders, payments, OLAP reports, and assembly charts.
    2
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    Enables searching and retrieving job vacancies, resumes, employers, and salary statistics from the hh.ru API (Russia/CIS) via 19 tools, with optional token access for resume search.
    19
    58 npm
    5
    MIT