Skip to main content
Glama
tools.py32.6 kB
""" MCP tools for Viterbit API functionality. """ import json import logging from typing import Any, Dict, List, Optional from mcp.types import Tool, TextContent from typing import Any from viterbit_client import ViterbitClient class ViterbitTools: """MCP tools for Viterbit API operations.""" def __init__(self, client: ViterbitClient): self.client = client def get_tools(self) -> List[Tool]: """Get all available MCP tools. Returns: List of MCP tool definitions """ return [ # Candidate Management Tools Tool( name="search_candidate", description="Search for a candidate by name, email address, or phone number. Returns basic candidate information including ID, name, email, and phone.", inputSchema={ "type": "object", "properties": { "search_term": { "type": "string", "description": "Candidate name, email address, or phone number to search for" } }, "required": ["search_term"] } ), Tool( name="get_candidate_details", description="Get full candidate details including custom fields and address information. Requires candidate ID.", inputSchema={ "type": "object", "properties": { "candidate_id": { "type": "string", "description": "The Viterbit candidate ID" } }, "required": ["candidate_id"] } ), Tool( name="get_candidate_with_filters", description="Get candidate details with enriched filtering data including subscription status, activity status, and location info. Useful for subscriber reports and filtering.", inputSchema={ "type": "object", "properties": { "email": { "type": "string", "description": "Email address of the candidate" } }, "required": ["email"] } ), # Job Management Tools Tool( name="get_job_details", description="Get comprehensive job information including custom fields, requirements, location, and salary details.", inputSchema={ "type": "object", "properties": { "job_id": { "type": "string", "description": "The Viterbit job ID" } }, "required": ["job_id"] } ), # Candidature Management Tools Tool( name="find_active_candidatures", description="Find all active job applications (candidatures) for a candidate by their email address.", inputSchema={ "type": "object", "properties": { "email": { "type": "string", "description": "Email address of the candidate" } }, "required": ["email"] } ), # Utility Tools Tool( name="get_custom_fields_definitions", description="Get all available custom field definitions and their schemas from Viterbit. Useful for understanding field structure and IDs.", inputSchema={ "type": "object", "properties": {}, "required": [] } ), Tool( name="check_candidate_eligibility", description="Check if a candidate should be included in reports based on their activity status and other filtering criteria.", inputSchema={ "type": "object", "properties": { "viterbit_data": { "type": "object", "description": "Candidate data object with Viterbit fields (typically from get_candidate_with_filters)" } }, "required": ["viterbit_data"] } ), Tool( name="get_department_location_mappings", description="Get the department and location ID mappings used by Viterbit. Returns both department names to IDs and location names to IDs.", inputSchema={ "type": "object", "properties": {}, "required": [] } ), Tool( name="extract_discord_username", description="Extract Discord username from a candidate's custom fields data.", inputSchema={ "type": "object", "properties": { "custom_fields": { "type": "array", "description": "Array of custom field objects from candidate details" } }, "required": ["custom_fields"] } ), Tool( name="search_subscribers", description="Search for candidates who are subscribers. Optionally filter by activity status, location, or other criteria. Returns candidates data plus metadata with total counts.", inputSchema={ "type": "object", "properties": { "is_subscriber": { "type": "boolean", "description": "Filter by subscription status (true for subscribers, false for non-subscribers)", "default": True }, "activity_status": { "type": "string", "description": "Filter by activity status (Activo/Inactivo)", "enum": ["Activo", "Inactivo"] }, "page": { "type": "number", "description": "Page number for pagination", "default": 1 }, "page_size": { "type": "number", "description": "Number of results per page", "default": 50 } }, "required": [] } ), Tool( name="get_candidate_count", description="Get the total count of candidates matching specific criteria without returning all the data. Perfect for answering 'how many candidates are...' questions.", inputSchema={ "type": "object", "properties": { "is_subscriber": { "type": "boolean", "description": "Filter by subscription status (true for subscribers, false for non-subscribers)" }, "activity_status": { "type": "string", "description": "Filter by activity status (Activo/Inactivo)", "enum": ["Activo", "Inactivo"] }, "coach": { "type": "string", "description": "Filter by coach (Alexia, Irene, Irina)", "enum": ["Alexia", "Irene", "Irina"] }, "has_driving_license": { "type": "string", "description": "Filter by driving license (Sí, No, Me lo estoy sacando)", "enum": ["Sí", "No", "Me lo estoy sacando"] }, "national_mobility": { "type": "string", "description": "Filter by national mobility (Sí, No, Puedo desplazarme pero no dormir fuera de casa)", "enum": ["Sí", "No", "Puedo desplazarme pero no dormir fuera de casa"] }, "has_experience": { "type": "string", "description": "Filter by related experience (Sí, No)", "enum": ["Sí", "No"] }, "zona": { "type": "string", "description": "Filter by zone/area (custom field Zona)" }, "provincia": { "type": "string", "description": "Filter by province (custom field Provincia)" }, "city": { "type": "string", "description": "Filter by city from address" }, "state": { "type": "string", "description": "Filter by state/region from address" }, "postal_code": { "type": "string", "description": "Filter by postal code from address" } }, "required": [] } ), Tool( name="search_candidates_by_location", description="Search candidates by geographic location using zones, provinces, cities, or address information.", inputSchema={ "type": "object", "properties": { "zona": { "type": "string", "description": "Search by zone/area (custom field)" }, "provincia": { "type": "string", "description": "Search by province (custom field)" }, "city": { "type": "string", "description": "Search by city from address" }, "state": { "type": "string", "description": "Search by state/region from address" }, "postal_code": { "type": "string", "description": "Search by postal code from address" }, "is_subscriber": { "type": "boolean", "description": "Also filter by subscription status" }, "activity_status": { "type": "string", "description": "Also filter by activity status (Activo/Inactivo)", "enum": ["Activo", "Inactivo"] }, "page": { "type": "number", "description": "Page number for pagination", "default": 1 }, "page_size": { "type": "number", "description": "Number of results per page", "default": 50 } }, "required": [] } ), # Candidature Stage History Tools Tool( name="get_candidature_stage_history", description="Get candidature details including complete stages history. Shows all stage transitions with timestamps.", inputSchema={ "type": "object", "properties": { "candidature_id": { "type": "string", "description": "The candidature ID to get stage history for" } }, "required": ["candidature_id"] } ), Tool( name="get_candidatures_changed_to_stage", description="Find all candidatures that changed to a specific stage (like 'Match') during a given month. Perfect for monthly reporting on stage transitions.", inputSchema={ "type": "object", "properties": { "stage_name": { "type": "string", "description": "Name of the stage to filter by (e.g., 'Match', 'Preseleccionado', 'Contratado')" }, "year": { "type": "integer", "description": "Year to filter by (e.g., 2025)", "minimum": 2020, "maximum": 2030 }, "month": { "type": "integer", "description": "Month to filter by (1-12)", "minimum": 1, "maximum": 12 } }, "required": ["stage_name", "year", "month"] } ), Tool( name="count_candidatures_changed_to_stage", description="Count how many candidatures changed to a specific stage during a given month. Returns just the count number for quick reporting.", inputSchema={ "type": "object", "properties": { "stage_name": { "type": "string", "description": "Name of the stage to filter by (e.g., 'Match', 'Preseleccionado', 'Contratado')" }, "year": { "type": "integer", "description": "Year to filter by (e.g., 2025)", "minimum": 2020, "maximum": 2030 }, "month": { "type": "integer", "description": "Month to filter by (1-12)", "minimum": 1, "maximum": 12 } }, "required": ["stage_name", "year", "month"] } ), Tool( name="get_candidatures_in_current_stage", description="Get all candidatures currently in a specific stage right now. Returns detailed candidature information for candidates in the specified stage at this moment.", inputSchema={ "type": "object", "properties": { "stage_name": { "type": "string", "description": "Name of the stage to filter by (e.g., 'Match', 'Preseleccionado', 'Contratado')" }, "page": { "type": "number", "description": "Page number for pagination", "default": 1 }, "page_size": { "type": "number", "description": "Number of results per page (max 100)", "default": 50 } }, "required": ["stage_name"] } ), Tool( name="count_candidatures_in_current_stage", description="Count how many candidatures are currently in a specific stage right now. Returns just the count number for quick reporting about current stage status.", inputSchema={ "type": "object", "properties": { "stage_name": { "type": "string", "description": "Name of the stage to filter by (e.g., 'Match', 'Preseleccionado', 'Contratado')" } }, "required": ["stage_name"] } ) ] async def handle_tool_call(self, name: str, arguments: Dict[str, Any]) -> List[TextContent]: """Handle MCP tool calls. Args: name: Tool name arguments: Tool arguments Returns: List of text content responses """ try: if name == "search_candidate": result = await self.client.search_candidate(str(arguments["search_term"])) return [TextContent(type="text", text=json.dumps(result, indent=2))] elif name == "get_candidate_details": result = await self.client.get_candidate_details(str(arguments["candidate_id"])) return [TextContent(type="text", text=json.dumps(result, indent=2))] elif name == "get_candidate_with_filters": result = await self.client.get_candidate_with_viterbit_fields(str(arguments["email"])) return [TextContent(type="text", text=json.dumps(result, indent=2))] elif name == "get_job_details": result = await self.client.get_job_details(str(arguments["job_id"])) return [TextContent(type="text", text=json.dumps(result, indent=2))] elif name == "find_active_candidatures": result = await self.client.find_active_candidatures_by_email(str(arguments["email"])) return [TextContent(type="text", text=json.dumps(result, indent=2))] elif name == "get_custom_fields_definitions": result = await self.client.get_custom_fields_definitions() return [TextContent(type="text", text=json.dumps(result, indent=2))] elif name == "check_candidate_eligibility": viterbit_data = arguments["viterbit_data"] if isinstance(viterbit_data, str): viterbit_data = json.loads(viterbit_data) result = self.client.should_include_candidate_in_report(viterbit_data) return [TextContent(type="text", text=json.dumps({ "eligible": result, "reason": "Candidate is inactive" if not result else "Candidate is eligible" }, indent=2))] elif name == "get_department_location_mappings": result = { "departments": self.client.get_department_mappings(), "locations": self.client.get_location_mappings() } return [TextContent(type="text", text=json.dumps(result, indent=2))] elif name == "extract_discord_username": custom_fields = arguments["custom_fields"] if isinstance(custom_fields, str): custom_fields = json.loads(custom_fields) result = self.client.extract_discord_user(custom_fields) return [TextContent(type="text", text=json.dumps({ "discord_username": result }, indent=2))] elif name == "search_subscribers": is_subscriber = arguments.get("is_subscriber", True) activity_status = arguments.get("activity_status") page = arguments.get("page", 1) page_size = arguments.get("page_size", 50) # Build filters for the search filters = {} # Add subscription filter using the custom field ID if is_subscriber is not None: filters["67fe75c8f8e7996e110cb5a0"] = is_subscriber # Add activity status filter if provided if activity_status: filters["68a455d5585b0d17c20bdcb1"] = activity_status result = await self.client.search_candidates_with_filters( filters=filters, page=page, page_size=page_size ) if result is None: return [TextContent(type="text", text="Error: Failed to search subscribers")] # Format the response to highlight the metadata meta = result.get("meta", {}) data = result.get("data", []) formatted_response = { "summary": { "total_found": meta.get("total", 0), "showing": len(data), "page": meta.get("page", 1), "total_pages": meta.get("total_pages", 0), "has_more": meta.get("has_more", False) }, "filters_applied": {k: v for k, v in arguments.items() if v is not None}, "candidates": data, "meta": meta } return [TextContent(type="text", text=json.dumps(formatted_response, indent=2))] elif name == "get_candidate_count": # Build filters based on provided criteria filters = {} # Subscription status is_subscriber = arguments.get("is_subscriber") if is_subscriber is not None: filters["67fe75c8f8e7996e110cb5a0"] = is_subscriber # Activity status activity_status = arguments.get("activity_status") if activity_status: filters["68a455d5585b0d17c20bdcb1"] = activity_status # Coach coach = arguments.get("coach") if coach: filters["68c14707fdded0284e03159c"] = coach # Driving license has_driving_license = arguments.get("has_driving_license") if has_driving_license: filters["6748889b1207a9f3040c4a8a"] = has_driving_license # National mobility national_mobility = arguments.get("national_mobility") if national_mobility: filters["67c8200c62e3ae6c1006691b"] = national_mobility # Experience has_experience = arguments.get("has_experience") if has_experience: filters["67c8412097b7cbb331024e09"] = has_experience # Location filters zona = arguments.get("zona") if zona: filters["67c83def159fcdd58906e4c5"] = zona # Zona field ID provincia = arguments.get("provincia") if provincia: filters["67c84b1c21bda2b3c60fabea"] = provincia # Provincia field ID # Address fields city = arguments.get("city") if city: filters["address__city"] = city state = arguments.get("state") if state: filters["address__state"] = state postal_code = arguments.get("postal_code") if postal_code: filters["address__postal_code"] = postal_code # Search with page_size=1 to get just the count efficiently result = await self.client.search_candidates_with_filters( filters=filters, page=1, page_size=1 ) if result is None: return [TextContent(type="text", text="Error: Failed to get candidate count")] # Extract and format the count information meta = result.get("meta", {}) count_summary = { "total_candidates": meta.get("total", 0), "filters_applied": {k: v for k, v in arguments.items() if v is not None}, "meta": meta } return [TextContent(type="text", text=json.dumps(count_summary, indent=2))] elif name == "search_candidates_by_location": # Build filters for location-based search filters = {} # Custom field location filters zona = arguments.get("zona") if zona: filters["67c83def159fcdd58906e4c5"] = zona # Zona field ID provincia = arguments.get("provincia") if provincia: filters["67c84b1c21bda2b3c60fabea"] = provincia # Provincia field ID # Address field filters city = arguments.get("city") if city: filters["address__city"] = city state = arguments.get("state") if state: filters["address__state"] = state postal_code = arguments.get("postal_code") if postal_code: filters["address__postal_code"] = postal_code # Optional additional filters is_subscriber = arguments.get("is_subscriber") if is_subscriber is not None: filters["67fe75c8f8e7996e110cb5a0"] = is_subscriber activity_status = arguments.get("activity_status") if activity_status: filters["68a455d5585b0d17c20bdcb1"] = activity_status # Pagination page = arguments.get("page", 1) page_size = arguments.get("page_size", 50) result = await self.client.search_candidates_with_filters( filters=filters, page=page, page_size=page_size ) if result is None: return [TextContent(type="text", text="Error: Failed to search candidates by location")] # Format the response to highlight the metadata meta = result.get("meta", {}) data = result.get("data", []) formatted_response = { "summary": { "total_found": meta.get("total", 0), "showing": len(data), "page": meta.get("page", 1), "total_pages": meta.get("total_pages", 0), "has_more": meta.get("has_more", False) }, "location_filters": {k: v for k, v in arguments.items() if v is not None and k in ["zona", "provincia", "city", "state", "postal_code"]}, "additional_filters": {k: v for k, v in arguments.items() if v is not None and k in ["is_subscriber", "activity_status"]}, "candidates": data, "meta": meta } return [TextContent(type="text", text=json.dumps(formatted_response, indent=2))] elif name == "get_candidature_stage_history": result = await self.client.get_candidature_with_stage_history(str(arguments["candidature_id"])) if result is None: return [TextContent(type="text", text="Error: Candidature not found or no access")] return [TextContent(type="text", text=json.dumps(result, indent=2))] elif name == "get_candidatures_changed_to_stage": stage_name = str(arguments["stage_name"]) year = int(arguments["year"]) month = int(arguments["month"]) result = await self.client.get_candidatures_changed_to_stage(stage_name, year, month) formatted_response = { "summary": { "total_found": len(result), "stage_name": stage_name, "period": f"{year}-{month:02d}", "search_criteria": { "stage_name": stage_name, "year": year, "month": month } }, "candidatures": result } return [TextContent(type="text", text=json.dumps(formatted_response, indent=2))] elif name == "count_candidatures_changed_to_stage": stage_name = str(arguments["stage_name"]) year = int(arguments["year"]) month = int(arguments["month"]) count = await self.client.count_candidatures_changed_to_stage(stage_name, year, month) formatted_response = { "count": count, "stage_name": stage_name, "period": f"{year}-{month:02d}", "query": f"Candidatures changed to '{stage_name}' in {year}-{month:02d}" } return [TextContent(type="text", text=json.dumps(formatted_response, indent=2))] elif name == "get_candidatures_in_current_stage": stage_name = str(arguments["stage_name"]) page = arguments.get("page", 1) page_size = arguments.get("page_size", 50) result = await self.client.get_candidatures_in_current_stage(stage_name, page, page_size) if result is None: return [TextContent(type="text", text="Error: Failed to get candidatures in current stage")] meta = result.get("meta", {}) data = result.get("data", []) formatted_response = { "summary": { "total_in_stage": meta.get("total", 0), "showing": len(data), "page": meta.get("page", 1), "total_pages": meta.get("total_pages", 0), "has_more": meta.get("has_more", False) }, "stage_name": stage_name, "query": f"Candidatures currently in '{stage_name}' stage", "candidatures": data, "meta": meta } return [TextContent(type="text", text=json.dumps(formatted_response, indent=2))] elif name == "count_candidatures_in_current_stage": stage_name = str(arguments["stage_name"]) count = await self.client.count_candidatures_in_current_stage(stage_name) formatted_response = { "count": count, "stage_name": stage_name, "query": f"Candidatures currently in '{stage_name}' stage" } return [TextContent(type="text", text=json.dumps(formatted_response, indent=2))] else: return [TextContent(type="text", text=f"Unknown tool: {name}")] except Exception as e: error_msg = f"Error executing {name}: {str(e)}" logging.error(error_msg) return [TextContent(type="text", text=f"Error: {error_msg}")]

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/Alex-air/mcp_viterbit'

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