Skip to main content
Glama

HeadHunter MCP Server

by gmen1057
server.py18.9 kB
#!/usr/bin/env python3 """HeadHunter MCP Server. This module provides a Model Context Protocol (MCP) server for interacting with the HeadHunter job search API. The server exposes various tools for searching vacancies, retrieving detailed job information, managing applications, and handling user authentication through OAuth. The module implements the MCP protocol to allow AI assistants to interact with HeadHunter's API services, including both public API endpoints (for searching vacancies and employers) and authenticated endpoints (for managing resumes and applications). Main components: - MCP Server setup with stdio transport - Tool definitions for various HeadHunter API operations - Request handling and response formatting - OAuth authentication support Tools: hh_search_vacancies: Search for job vacancies with various filters hh_get_vacancy: Get detailed information about a specific vacancy hh_get_employer: Retrieve employer/company information hh_get_similar: Find similar vacancies for a given vacancy hh_get_areas: Get list of available regions/areas for filtering hh_get_dictionaries: Retrieve all filter dictionaries from HeadHunter hh_apply_to_vacancy: Submit job applications (requires OAuth) hh_get_negotiations: Get user's application history (requires OAuth) hh_get_resumes: List user's resumes (requires OAuth) hh_get_resume: Get detailed resume information (requires OAuth) """ import asyncio import json from typing import Any from dotenv import load_dotenv from mcp.server import Server from mcp.types import ( Tool, TextContent, ImageContent, EmbeddedResource, ) import mcp.server.stdio from hh_client import HHClient load_dotenv() app = Server("hh-api") hh_client = HHClient() @app.list_tools() async def list_tools() -> list[Tool]: """Return the list of available MCP tools for HeadHunter API. This function defines all the tools that can be called by MCP clients to interact with the HeadHunter API. It includes both public tools (for searching vacancies and getting employer information) and authenticated tools (for managing applications and resumes). The tools are organized into several categories: - Vacancy search and retrieval - Employer information - Similar vacancies - Reference data (areas, dictionaries) - User operations (applications, resumes) - require OAuth Returns: list[Tool]: A list of Tool objects defining the available MCP tools, each with its name, description, and input schema. """ return [ Tool( name="hh_search_vacancies", description="Search for job vacancies on HeadHunter. Returns list of vacancies matching criteria.", inputSchema={ "type": "object", "properties": { "text": { "type": "string", "description": "Search query (job title, keywords, skills)", }, "area": { "type": "integer", "description": "Region ID (1=Moscow, 2=SPb, 113=Russia). Use hh_get_areas to find IDs.", }, "experience": { "type": "string", "enum": [ "noExperience", "between1And3", "between3And6", "moreThan6", ], "description": "Required experience level", }, "employment": { "type": "string", "enum": ["full", "part", "project", "volunteer", "probation"], "description": "Employment type", }, "schedule": { "type": "string", "enum": [ "fullDay", "shift", "flexible", "remote", "flyInFlyOut", ], "description": "Work schedule", }, "salary": {"type": "integer", "description": "Minimum salary"}, "only_with_salary": { "type": "boolean", "description": "Show only vacancies with specified salary", }, "per_page": { "type": "integer", "description": "Results per page (max 100)", "default": 20, }, "page": { "type": "integer", "description": "Page number (0-indexed)", "default": 0, }, }, }, ), Tool( name="hh_get_vacancy", description="Get detailed information about specific vacancy by ID", inputSchema={ "type": "object", "properties": { "vacancy_id": { "type": "string", "description": "Vacancy ID from search results", } }, "required": ["vacancy_id"], }, ), Tool( name="hh_get_employer", description="Get information about employer/company by ID", inputSchema={ "type": "object", "properties": { "employer_id": { "type": "string", "description": "Employer ID from vacancy data", } }, "required": ["employer_id"], }, ), Tool( name="hh_get_similar", description="Get similar vacancies for a specific vacancy", inputSchema={ "type": "object", "properties": { "vacancy_id": {"type": "string", "description": "Vacancy ID"} }, "required": ["vacancy_id"], }, ), Tool( name="hh_get_areas", description="Get list of all available regions/areas with IDs for filtering", inputSchema={"type": "object", "properties": {}}, ), Tool( name="hh_get_dictionaries", description="Get all dictionaries (experience, employment, schedule, etc.) for filtering", inputSchema={"type": "object", "properties": {}}, ), Tool( name="hh_apply_to_vacancy", description="Apply to vacancy (requires OAuth authentication). User must authorize first.", inputSchema={ "type": "object", "properties": { "vacancy_id": { "type": "string", "description": "Vacancy ID to apply to", }, "resume_id": { "type": "string", "description": "Resume ID to use for application", }, "letter": { "type": "string", "description": "Cover letter text (optional)", }, }, "required": ["vacancy_id", "resume_id"], }, ), Tool( name="hh_get_negotiations", description="Get user's application history and status (requires OAuth). Supports pagination.", inputSchema={ "type": "object", "properties": { "per_page": { "type": "integer", "description": "Results per page (max 100)", "default": 20, }, "page": { "type": "integer", "description": "Page number (0-indexed)", "default": 0, }, }, }, ), Tool( name="hh_get_resumes", description="Get list of user's resumes (requires OAuth)", inputSchema={"type": "object", "properties": {}}, ), Tool( name="hh_get_resume", description="Get detailed information about specific resume (requires OAuth)", inputSchema={ "type": "object", "properties": { "resume_id": {"type": "string", "description": "Resume ID"} }, "required": ["resume_id"], }, ), ] @app.call_tool() async def call_tool( name: str, arguments: Any ) -> list[TextContent | ImageContent | EmbeddedResource]: """Execute a HeadHunter API tool call. This function serves as the main dispatcher for all HeadHunter MCP tool calls. It handles the execution of various tools including vacancy search, detailed information retrieval, application management, and resume operations. The function processes the tool arguments, makes the appropriate API calls through the HHClient, and formats the responses for the MCP client. The function handles both public API calls (no authentication required) and authenticated calls (OAuth token required). For authenticated calls, appropriate error handling is provided when tokens are missing or invalid. Args: name (str): The name of the tool to execute. Must be one of the supported tool names defined in list_tools(). arguments (Any): The arguments for the tool call. The structure depends on the specific tool being called and matches the inputSchema defined for that tool. Returns: list[TextContent | ImageContent | EmbeddedResource]: A list containing the formatted response from the HeadHunter API. Typically contains a single TextContent object with the formatted result data. Raises: Exception: Various exceptions may be raised during API calls, authentication issues, or data processing errors. All exceptions are caught and returned as error messages to the MCP client. """ try: if name == "hh_search_vacancies": result = await hh_client.search_vacancies(**arguments) items = result.get("items", []) summary = f"Found {result.get('found', 0)} vacancies (showing {len(items)})" formatted_items = [] for item in items[:10]: salary_info = "Not specified" if item.get("salary"): sal = item["salary"] from_sal = sal.get("from", "") to_sal = sal.get("to", "") currency = sal.get("currency", "") if from_sal and to_sal: salary_info = f"{from_sal}-{to_sal} {currency}" elif from_sal: salary_info = f"from {from_sal} {currency}" elif to_sal: salary_info = f"up to {to_sal} {currency}" formatted_items.append( f"[{item['id']}] {item['name']}\n" f" Company: {item.get('employer', {}).get('name', 'N/A')}\n" f" Salary: {salary_info}\n" f" Area: {item.get('area', {}).get('name', 'N/A')}\n" f" URL: {item.get('alternate_url', 'N/A')}\n" ) return [ TextContent( type="text", text=f"{summary}\n\n" + "\n".join(formatted_items) ) ] elif name == "hh_get_vacancy": result = await hh_client.get_vacancy(arguments["vacancy_id"]) salary_info = "Not specified" if result.get("salary"): sal = result["salary"] from_sal = sal.get("from", "") to_sal = sal.get("to", "") currency = sal.get("currency", "") if from_sal and to_sal: salary_info = f"{from_sal}-{to_sal} {currency}" elif from_sal: salary_info = f"from {from_sal} {currency}" elif to_sal: salary_info = f"up to {to_sal} {currency}" formatted = f""" Vacancy: {result.get('name')} Company: {result.get('employer', {}).get('name', 'N/A')} Salary: {salary_info} Area: {result.get('area', {}).get('name', 'N/A')} Experience: {result.get('experience', {}).get('name', 'N/A')} Employment: {result.get('employment', {}).get('name', 'N/A')} Schedule: {result.get('schedule', {}).get('name', 'N/A')} Description: {result.get('description', 'No description')} Key Skills: {', '.join([s.get('name', '') for s in result.get('key_skills', [])])} URL: {result.get('alternate_url', 'N/A')} """ return [TextContent(type="text", text=formatted)] elif name == "hh_get_employer": result = await hh_client.get_employer(arguments["employer_id"]) return [ TextContent( type="text", text=json.dumps(result, indent=2, ensure_ascii=False) ) ] elif name == "hh_get_similar": result = await hh_client.get_similar_vacancies(arguments["vacancy_id"]) items = result.get("items", []) formatted_items = [] for item in items: formatted_items.append( f"[{item['id']}] {item['name']} - {item.get('employer', {}).get('name', 'N/A')}" ) return [ TextContent( type="text", text="\n".join(formatted_items) if formatted_items else "No similar vacancies found", ) ] elif name == "hh_get_areas": result = await hh_client.get_areas() return [ TextContent( type="text", text=json.dumps(result, indent=2, ensure_ascii=False) ) ] elif name == "hh_get_dictionaries": result = await hh_client.get_dictionaries() return [ TextContent( type="text", text=json.dumps(result, indent=2, ensure_ascii=False) ) ] elif name == "hh_apply_to_vacancy": result = await hh_client.apply_to_vacancy(**arguments) return [ TextContent( type="text", text=f"Application submitted successfully!\n{json.dumps(result, indent=2, ensure_ascii=False)}", ) ] elif name == "hh_get_negotiations": per_page = arguments.get("per_page", 20) page = arguments.get("page", 0) result = await hh_client.get_negotiations(per_page=per_page, page=page) items = result.get("items", []) total = result.get("found", 0) summary = f"Total applications: {total} (showing page {page}, {len(items)} items)\n\n" formatted_items = [] for item in items: vacancy = item.get("vacancy", {}) state = item.get("state", {}).get("name", "Unknown") created = item.get("created_at", "N/A") formatted_items.append( f"[{vacancy.get('id', 'N/A')}] {vacancy.get('name', 'N/A')}\n" f" Company: {vacancy.get('employer', {}).get('name', 'N/A')}\n" f" Status: {state}\n" f" Applied: {created}\n" ) return [TextContent(type="text", text=summary + "\n".join(formatted_items))] elif name == "hh_get_resumes": result = await hh_client.get_resumes() items = result.get("items", []) formatted_items = [] for item in items: status = ( "✅ Published" if item.get("status", {}).get("id") == "published" else "⏸️ Not published" ) formatted_items.append( f"[{item['id']}] {item.get('title', 'No title')}\n" f" Status: {status}\n" f" Updated: {item.get('updated_at', 'N/A')}\n" f" Views: {item.get('views_count', 0)}\n" ) return [ TextContent( type="text", text=f"Your resumes ({len(items)}):\n\n" + "\n".join(formatted_items), ) ] elif name == "hh_get_resume": result = await hh_client.get_resume(arguments["resume_id"]) formatted = f""" Resume: {result.get('title', 'No title')} Status: {result.get('status', {}).get('name', 'N/A')} Updated: {result.get('updated_at', 'N/A')} Views: {result.get('views_count', 0)} Experience: """ for exp in result.get("experience", []): formatted += ( f"- {exp.get('company', 'N/A')}: {exp.get('position', 'N/A')}\n" ) formatted += f"\nSkills: {result.get('skills', 'N/A')}" return [TextContent(type="text", text=formatted)] else: return [TextContent(type="text", text=f"Unknown tool: {name}")] except Exception as e: return [TextContent(type="text", text=f"Error: {str(e)}")] async def main(): """Initialize and run the HeadHunter MCP server. This function serves as the entry point for the HeadHunter MCP server. It sets up the standard input/output (stdio) transport for communication with MCP clients and starts the server with the necessary initialization options. The server runs indefinitely, handling incoming MCP requests until terminated. The stdio transport allows the server to communicate with MCP clients through standard input and output streams, making it suitable for use with AI assistants and other MCP-compatible applications. """ async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): await app.run(read_stream, write_stream, app.create_initialization_options()) if __name__ == "__main__": asyncio.run(main())

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/gmen1057/headhunter-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server