Skip to main content
Glama

MCP-Odoo

by iaisep
crm.pyโ€ข42 kB
""" CRM resources for MCP-Odoo This module provides MCP tools and resources for comprehensive CRM management, including leads, opportunities, partners, teams, and University ISEP specific fields. """ from typing import Optional, List, Dict, Any, Union from pydantic import BaseModel from datetime import datetime, date from mcp.server.fastmcp import Context from mcp_instance import mcp from context_handler import get_odoo_client_from_context # Models for request/response types class LeadFilter(BaseModel): """Filter parameters for lead/opportunity listing""" partner_id: Optional[int] = None team_id: Optional[int] = None user_id: Optional[int] = None stage_id: Optional[int] = None type: Optional[str] = None # 'lead' or 'opportunity' priority: Optional[str] = None state: Optional[str] = None date_from: Optional[str] = None date_to: Optional[str] = None program_id: Optional[int] = None # x_studio_programa_academico canal_contacto: Optional[str] = None # x_studio_canal_de_contacto limit: Optional[int] = 100 class LeadCreate(BaseModel): """Model for creating new leads""" name: str contact_name: Optional[str] = None email_from: Optional[str] = None phone: Optional[str] = None partner_name: Optional[str] = None description: Optional[str] = None team_id: Optional[int] = None user_id: Optional[int] = None stage_id: Optional[int] = None expected_revenue: Optional[float] = None probability: Optional[float] = None program_id: Optional[int] = None canal_contacto: Optional[str] = None programa_interes: Optional[str] = None class PartnerFilter(BaseModel): """Filter parameters for partner listing""" name: Optional[str] = None email: Optional[str] = None phone: Optional[str] = None is_company: Optional[bool] = None customer_rank: Optional[int] = None supplier_rank: Optional[int] = None category_id: Optional[int] = None country_id: Optional[int] = None limit: Optional[int] = 100 # Helper formatting functions def format_lead(lead: Dict[str, Any]) -> Dict[str, Any]: """Format lead data for better presentation""" result = { "id": lead["id"], "name": lead["name"], "type": lead.get("type", "lead"), "contact_name": lead.get("contact_name", ""), "partner_name": lead.get("partner_name", ""), "email_from": lead.get("email_from", ""), "phone": lead.get("phone", ""), "mobile": lead.get("mobile", ""), "expected_revenue": lead.get("expected_revenue", 0.0), "probability": lead.get("probability", 0.0), "priority": lead.get("priority", "0"), "create_date": lead.get("create_date", ""), "write_date": lead.get("write_date", ""), "date_deadline": lead.get("date_deadline", ""), "stage": { "id": lead["stage_id"][0] if lead.get("stage_id") else None, "name": lead["stage_id"][1] if lead.get("stage_id") else None } if lead.get("stage_id") else None, "team": { "id": lead["team_id"][0] if lead.get("team_id") else None, "name": lead["team_id"][1] if lead.get("team_id") else None } if lead.get("team_id") else None, "user": { "id": lead["user_id"][0] if lead.get("user_id") else None, "name": lead["user_id"][1] if lead.get("user_id") else None } if lead.get("user_id") else None, "partner": { "id": lead["partner_id"][0] if lead.get("partner_id") else None, "name": lead["partner_id"][1] if lead.get("partner_id") else None } if lead.get("partner_id") else None, # University ISEP specific fields "programa_academico": { "id": lead["x_studio_programa_academico"][0] if lead.get("x_studio_programa_academico") else None, "name": lead["x_studio_programa_academico"][1] if lead.get("x_studio_programa_academico") else None } if lead.get("x_studio_programa_academico") else None, "canal_contacto": lead.get("x_studio_canal_de_contacto", ""), "programa_interes": lead.get("x_studio_programa_de_inters", ""), "fecha_firma": lead.get("x_studio_fecha_de_firma", ""), "progress": lead.get("progress", 0.0), "mautic_export": lead.get("mautic_export", False), "mautic_id": lead.get("x_studio_id_mautic", ""), # Google Analytics fields "gr_source": lead.get("gr_source", ""), "gr_campaign": lead.get("gr_campaingn", ""), "gr_term": lead.get("gr_term", ""), "description": lead.get("description", ""), } return result def format_partner(partner: Dict[str, Any]) -> Dict[str, Any]: """Format partner data for better presentation""" return { "id": partner["id"], "name": partner["name"], "display_name": partner.get("display_name", ""), "email": partner.get("email", ""), "phone": partner.get("phone", ""), "mobile": partner.get("mobile", ""), "website": partner.get("website", ""), "is_company": partner.get("is_company", False), "customer_rank": partner.get("customer_rank", 0), "supplier_rank": partner.get("supplier_rank", 0), "vat": partner.get("vat", ""), "street": partner.get("street", ""), "street2": partner.get("street2", ""), "city": partner.get("city", ""), "zip": partner.get("zip", ""), "country": { "id": partner["country_id"][0] if partner.get("country_id") else None, "name": partner["country_id"][1] if partner.get("country_id") else None } if partner.get("country_id") else None, "state": { "id": partner["state_id"][0] if partner.get("state_id") else None, "name": partner["state_id"][1] if partner.get("state_id") else None } if partner.get("state_id") else None, "parent": { "id": partner["parent_id"][0] if partner.get("parent_id") else None, "name": partner["parent_id"][1] if partner.get("parent_id") else None } if partner.get("parent_id") else None, "categories": [ {"id": cat[0], "name": cat[1]} for cat in partner.get("category_id", []) ] if isinstance(partner.get("category_id"), list) else [], "create_date": partner.get("create_date", ""), "write_date": partner.get("write_date", ""), "active": partner.get("active", True), } # ============================================================================ # LEAD/OPPORTUNITY MANAGEMENT TOOLS # ============================================================================ @mcp.tool() async def list_leads(ctx: Context, partner_id: Optional[int] = None, team_id: Optional[int] = None, user_id: Optional[int] = None, stage_id: Optional[int] = None, type: Optional[str] = None, priority: Optional[str] = None, date_from: Optional[str] = None, date_to: Optional[str] = None, program_id: Optional[int] = None, canal_contacto: Optional[str] = None, limit: Optional[int] = 100) -> List[Dict[str, Any]]: """ List leads/opportunities with comprehensive filtering options. Args: partner_id: Filter by partner/customer ID team_id: Filter by sales team ID user_id: Filter by salesperson ID stage_id: Filter by stage ID type: Filter by type ('lead' or 'opportunity') priority: Filter by priority ('0', '1', '2', '3') date_from: Filter from this date (YYYY-MM-DD) date_to: Filter until this date (YYYY-MM-DD) program_id: Filter by academic program ID (ISEP specific) canal_contacto: Filter by contact channel (ISEP specific) limit: Maximum number of records to return Returns: List of leads with detailed information including ISEP custom fields """ try: # Get Odoo client odoo_client = await get_odoo_client_from_context(ctx) # Build domain filter domain = [] if partner_id: domain.append(("partner_id", "=", partner_id)) if team_id: domain.append(("team_id", "=", team_id)) if user_id: domain.append(("user_id", "=", user_id)) if stage_id: domain.append(("stage_id", "=", stage_id)) if type: domain.append(("type", "=", type)) if priority: domain.append(("priority", "=", priority)) if date_from: domain.append(("create_date", ">=", date_from)) if date_to: domain.append(("create_date", "<=", date_to)) if program_id: domain.append(("x_studio_programa_academico", "=", program_id)) if canal_contacto: domain.append(("x_studio_canal_de_contacto", "ilike", canal_contacto)) await ctx.info(f"Fetching leads with domain: {domain}") # Fields to retrieve fields = [ "id", "name", "type", "contact_name", "partner_name", "email_from", "phone", "mobile", "expected_revenue", "probability", "priority", "create_date", "write_date", "date_deadline", "stage_id", "team_id", "user_id", "partner_id", "description", # ISEP specific fields "x_studio_programa_academico", "x_studio_canal_de_contacto", "x_studio_programa_de_inters", "x_studio_fecha_de_firma", "progress", "mautic_export", "x_studio_id_mautic", # Google Analytics fields "gr_source", "gr_campaingn", "gr_term" ] # Query Odoo leads = await odoo_client.search_read( "crm.lead", domain, fields, limit=limit, order="create_date desc" ) # Format response return [format_lead(lead) for lead in leads] except Exception as e: await ctx.error(f"Error fetching leads: {str(e)}") return {"error": str(e)} @mcp.tool() async def get_lead_details(ctx: Context, lead_id: int) -> Dict[str, Any]: """ Get detailed information about a specific lead/opportunity. Args: lead_id: ID of the lead to retrieve Returns: Detailed lead information including all custom fields """ try: odoo_client = await get_odoo_client_from_context(ctx) # Get all available fields for the lead lead_data = await odoo_client.execute_kw( "crm.lead", "read", [lead_id], {} ) if not lead_data: return {"error": f"Lead with ID {lead_id} not found"} lead = lead_data[0] result = format_lead(lead) # Add additional detailed fields result.update({ "website": lead.get("website", ""), "function": lead.get("function", ""), "street": lead.get("street", ""), "street2": lead.get("street2", ""), "city": lead.get("city", ""), "zip": lead.get("zip", ""), "date_open": lead.get("date_open", ""), "date_closed": lead.get("date_closed", ""), "date_last_stage_update": lead.get("date_last_stage_update", ""), "won_status": lead.get("won_status", ""), "active": lead.get("active", True), "color": lead.get("color", 0), # Additional ISEP fields "duracion_convenio": lead.get("x_studio_duracin_de_convenio", ""), "correo_existe": lead.get("x_studio_correo_existe", False), "correo_revisado": lead.get("x_studio_correo_revisado", False), "bool_interes": lead.get("x_studio_bool_interes", False), }) return result except Exception as e: await ctx.error(f"Error fetching lead details: {str(e)}") return {"error": str(e)} @mcp.tool() async def create_lead(ctx: Context, name: str, contact_name: Optional[str] = None, email_from: Optional[str] = None, phone: Optional[str] = None, partner_name: Optional[str] = None, description: Optional[str] = None, team_id: Optional[int] = None, user_id: Optional[int] = None, stage_id: Optional[int] = None, expected_revenue: Optional[float] = None, probability: Optional[float] = None, program_id: Optional[int] = None, canal_contacto: Optional[str] = None, programa_interes: Optional[str] = None) -> Dict[str, Any]: """ Create a new lead/opportunity in the CRM. Args: name: Lead/Opportunity name (required) contact_name: Contact person name email_from: Email address phone: Phone number partner_name: Company name description: Description/notes team_id: Sales team ID user_id: Salesperson ID stage_id: Stage ID expected_revenue: Expected revenue probability: Probability (0-100) program_id: Academic program ID (ISEP specific) canal_contacto: Contact channel (ISEP specific) programa_interes: Program of interest (ISEP specific) Returns: Created lead information """ try: odoo_client = await get_odoo_client_from_context(ctx) # Prepare lead data lead_data = { "name": name, "type": "lead" # Default to lead, can be converted later } # Add optional fields if contact_name: lead_data["contact_name"] = contact_name if email_from: lead_data["email_from"] = email_from if phone: lead_data["phone"] = phone if partner_name: lead_data["partner_name"] = partner_name if description: lead_data["description"] = description if team_id: lead_data["team_id"] = team_id if user_id: lead_data["user_id"] = user_id if stage_id: lead_data["stage_id"] = stage_id if expected_revenue: lead_data["expected_revenue"] = expected_revenue if probability: lead_data["probability"] = probability # ISEP specific fields if program_id: lead_data["x_studio_programa_academico"] = program_id if canal_contacto: lead_data["x_studio_canal_de_contacto"] = canal_contacto if programa_interes: lead_data["x_studio_programa_de_inters"] = programa_interes await ctx.info(f"Creating lead with data: {lead_data}") # Create the lead lead_id = await odoo_client.execute_kw( "crm.lead", "create", [lead_data] ) # Get the created lead details created_lead = await get_lead_details(ctx, lead_id) await ctx.info(f"Successfully created lead with ID: {lead_id}") return created_lead except Exception as e: await ctx.error(f"Error creating lead: {str(e)}") return {"error": str(e)} @mcp.tool() async def update_lead(ctx: Context, lead_id: int, name: Optional[str] = None, contact_name: Optional[str] = None, email_from: Optional[str] = None, phone: Optional[str] = None, description: Optional[str] = None, stage_id: Optional[int] = None, user_id: Optional[int] = None, team_id: Optional[int] = None, expected_revenue: Optional[float] = None, probability: Optional[float] = None, priority: Optional[str] = None, program_id: Optional[int] = None, canal_contacto: Optional[str] = None, programa_interes: Optional[str] = None, progress: Optional[float] = None) -> Dict[str, Any]: """ Update an existing lead/opportunity. Args: lead_id: ID of the lead to update (required) name: Lead/Opportunity name contact_name: Contact person name email_from: Email address phone: Phone number description: Description/notes stage_id: Stage ID user_id: Salesperson ID team_id: Sales team ID expected_revenue: Expected revenue probability: Probability (0-100) priority: Priority ('0', '1', '2', '3') program_id: Academic program ID (ISEP specific) canal_contacto: Contact channel (ISEP specific) programa_interes: Program of interest (ISEP specific) progress: Progress percentage (ISEP specific) Returns: Updated lead information """ try: odoo_client = await get_odoo_client_from_context(ctx) # Prepare update data update_data = {} # Add fields to update if provided if name is not None: update_data["name"] = name if contact_name is not None: update_data["contact_name"] = contact_name if email_from is not None: update_data["email_from"] = email_from if phone is not None: update_data["phone"] = phone if description is not None: update_data["description"] = description if stage_id is not None: update_data["stage_id"] = stage_id if user_id is not None: update_data["user_id"] = user_id if team_id is not None: update_data["team_id"] = team_id if expected_revenue is not None: update_data["expected_revenue"] = expected_revenue if probability is not None: update_data["probability"] = probability if priority is not None: update_data["priority"] = priority # ISEP specific fields if program_id is not None: update_data["x_studio_programa_academico"] = program_id if canal_contacto is not None: update_data["x_studio_canal_de_contacto"] = canal_contacto if programa_interes is not None: update_data["x_studio_programa_de_inters"] = programa_interes if progress is not None: update_data["progress"] = progress if not update_data: return {"error": "No fields provided for update"} await ctx.info(f"Updating lead {lead_id} with data: {update_data}") # Update the lead await odoo_client.execute_kw( "crm.lead", "write", [[lead_id], update_data] ) # Get updated lead details updated_lead = await get_lead_details(ctx, lead_id) await ctx.info(f"Successfully updated lead {lead_id}") return updated_lead except Exception as e: await ctx.error(f"Error updating lead: {str(e)}") return {"error": str(e)} @mcp.tool() async def convert_lead_to_opportunity(ctx: Context, lead_id: int, partner_id: Optional[int] = None, user_id: Optional[int] = None, team_id: Optional[int] = None) -> Dict[str, Any]: """ Convert a lead to opportunity. Args: lead_id: ID of the lead to convert partner_id: Partner ID to associate with opportunity user_id: Salesperson ID to assign team_id: Sales team ID to assign Returns: Updated opportunity information """ try: odoo_client = await get_odoo_client_from_context(ctx) # Update lead to opportunity type update_data = {"type": "opportunity"} if partner_id: update_data["partner_id"] = partner_id if user_id: update_data["user_id"] = user_id if team_id: update_data["team_id"] = team_id await ctx.info(f"Converting lead {lead_id} to opportunity") await odoo_client.execute_kw( "crm.lead", "write", [[lead_id], update_data] ) # Get updated opportunity details opportunity = await get_lead_details(ctx, lead_id) await ctx.info(f"Successfully converted lead {lead_id} to opportunity") return opportunity except Exception as e: await ctx.error(f"Error converting lead to opportunity: {str(e)}") return {"error": str(e)} # ============================================================================ # PARTNER MANAGEMENT TOOLS # ============================================================================ @mcp.tool() async def list_partners(ctx: Context, name: Optional[str] = None, email: Optional[str] = None, phone: Optional[str] = None, is_company: Optional[bool] = None, customer_rank: Optional[int] = None, supplier_rank: Optional[int] = None, category_id: Optional[int] = None, country_id: Optional[int] = None, limit: Optional[int] = 100) -> List[Dict[str, Any]]: """ List partners/contacts with filtering options. Args: name: Filter by name (partial match) email: Filter by email (partial match) phone: Filter by phone (partial match) is_company: Filter companies (True) or individuals (False) customer_rank: Filter by customer rank (> 0 for customers) supplier_rank: Filter by supplier rank (> 0 for suppliers) category_id: Filter by category ID country_id: Filter by country ID limit: Maximum number of records to return Returns: List of partners with detailed information """ try: odoo_client = await get_odoo_client_from_context(ctx) # Build domain filter domain = [] if name: domain.append(("name", "ilike", name)) if email: domain.append(("email", "ilike", email)) if phone: domain.append(("phone", "ilike", phone)) if is_company is not None: domain.append(("is_company", "=", is_company)) if customer_rank is not None: domain.append(("customer_rank", ">=", customer_rank)) if supplier_rank is not None: domain.append(("supplier_rank", ">=", supplier_rank)) if category_id: domain.append(("category_id", "in", [category_id])) if country_id: domain.append(("country_id", "=", country_id)) await ctx.info(f"Fetching partners with domain: {domain}") # Fields to retrieve fields = [ "id", "name", "display_name", "email", "phone", "mobile", "website", "is_company", "customer_rank", "supplier_rank", "vat", "street", "street2", "city", "zip", "country_id", "state_id", "parent_id", "category_id", "create_date", "write_date", "active" ] # Query Odoo partners = await odoo_client.search_read( "res.partner", domain, fields, limit=limit, order="name asc" ) # Format response return [format_partner(partner) for partner in partners] except Exception as e: await ctx.error(f"Error fetching partners: {str(e)}") return {"error": str(e)} @mcp.tool() async def get_partner_details(ctx: Context, partner_id: int) -> Dict[str, Any]: """ Get detailed information about a specific partner. Args: partner_id: ID of the partner to retrieve Returns: Detailed partner information """ try: odoo_client = await get_odoo_client_from_context(ctx) partner_data = await odoo_client.execute_kw( "res.partner", "read", [partner_id], {} ) if not partner_data: return {"error": f"Partner with ID {partner_id} not found"} partner = partner_data[0] result = format_partner(partner) # Add additional detailed fields result.update({ "function": partner.get("function", ""), "title": { "id": partner["title"][0] if partner.get("title") else None, "name": partner["title"][1] if partner.get("title") else None } if partner.get("title") else None, "lang": partner.get("lang", ""), "tz": partner.get("tz", ""), "comment": partner.get("comment", ""), "ref": partner.get("ref", ""), "industry": { "id": partner["industry_id"][0] if partner.get("industry_id") else None, "name": partner["industry_id"][1] if partner.get("industry_id") else None } if partner.get("industry_id") else None, "company": { "id": partner["company_id"][0] if partner.get("company_id") else None, "name": partner["company_id"][1] if partner.get("company_id") else None } if partner.get("company_id") else None, }) return result except Exception as e: await ctx.error(f"Error fetching partner details: {str(e)}") return {"error": str(e)} @mcp.tool() async def create_partner(ctx: Context, name: str, email: Optional[str] = None, phone: Optional[str] = None, mobile: Optional[str] = None, is_company: Optional[bool] = False, website: Optional[str] = None, vat: Optional[str] = None, street: Optional[str] = None, street2: Optional[str] = None, city: Optional[str] = None, zip: Optional[str] = None, country_id: Optional[int] = None, state_id: Optional[int] = None, parent_id: Optional[int] = None, customer_rank: Optional[int] = 0, supplier_rank: Optional[int] = 0, category_ids: Optional[List[int]] = None) -> Dict[str, Any]: """ Create a new partner/contact. Args: name: Partner name (required) email: Email address phone: Phone number mobile: Mobile number is_company: True for companies, False for individuals website: Website URL vat: VAT/Tax ID street: Street address street2: Street address line 2 city: City zip: ZIP/Postal code country_id: Country ID state_id: State ID parent_id: Parent company ID customer_rank: Customer rank (0=not customer, 1+=customer) supplier_rank: Supplier rank (0=not supplier, 1+=supplier) category_ids: List of category IDs Returns: Created partner information """ try: odoo_client = await get_odoo_client_from_context(ctx) # Prepare partner data partner_data = { "name": name, "is_company": is_company or False } # Add optional fields if email: partner_data["email"] = email if phone: partner_data["phone"] = phone if mobile: partner_data["mobile"] = mobile if website: partner_data["website"] = website if vat: partner_data["vat"] = vat if street: partner_data["street"] = street if street2: partner_data["street2"] = street2 if city: partner_data["city"] = city if zip: partner_data["zip"] = zip if country_id: partner_data["country_id"] = country_id if state_id: partner_data["state_id"] = state_id if parent_id: partner_data["parent_id"] = parent_id if customer_rank is not None: partner_data["customer_rank"] = customer_rank if supplier_rank is not None: partner_data["supplier_rank"] = supplier_rank if category_ids: partner_data["category_id"] = [(6, 0, category_ids)] await ctx.info(f"Creating partner with data: {partner_data}") # Create the partner partner_id = await odoo_client.execute_kw( "res.partner", "create", [partner_data] ) # Get the created partner details created_partner = await get_partner_details(ctx, partner_id) await ctx.info(f"Successfully created partner with ID: {partner_id}") return created_partner except Exception as e: await ctx.error(f"Error creating partner: {str(e)}") return {"error": str(e)} @mcp.tool() async def update_partner(ctx: Context, partner_id: int, name: Optional[str] = None, email: Optional[str] = None, phone: Optional[str] = None, mobile: Optional[str] = None, website: Optional[str] = None, vat: Optional[str] = None, street: Optional[str] = None, street2: Optional[str] = None, city: Optional[str] = None, zip: Optional[str] = None, country_id: Optional[int] = None, state_id: Optional[int] = None, customer_rank: Optional[int] = None, supplier_rank: Optional[int] = None, active: Optional[bool] = None) -> Dict[str, Any]: """ Update an existing partner/contact. Args: partner_id: ID of the partner to update (required) name: Partner name email: Email address phone: Phone number mobile: Mobile number website: Website URL vat: VAT/Tax ID street: Street address street2: Street address line 2 city: City zip: ZIP/Postal code country_id: Country ID state_id: State ID customer_rank: Customer rank supplier_rank: Supplier rank active: Active status Returns: Updated partner information """ try: odoo_client = await get_odoo_client_from_context(ctx) # Prepare update data update_data = {} # Add fields to update if provided if name is not None: update_data["name"] = name if email is not None: update_data["email"] = email if phone is not None: update_data["phone"] = phone if mobile is not None: update_data["mobile"] = mobile if website is not None: update_data["website"] = website if vat is not None: update_data["vat"] = vat if street is not None: update_data["street"] = street if street2 is not None: update_data["street2"] = street2 if city is not None: update_data["city"] = city if zip is not None: update_data["zip"] = zip if country_id is not None: update_data["country_id"] = country_id if state_id is not None: update_data["state_id"] = state_id if customer_rank is not None: update_data["customer_rank"] = customer_rank if supplier_rank is not None: update_data["supplier_rank"] = supplier_rank if active is not None: update_data["active"] = active if not update_data: return {"error": "No fields provided for update"} await ctx.info(f"Updating partner {partner_id} with data: {update_data}") # Update the partner await odoo_client.execute_kw( "res.partner", "write", [[partner_id], update_data] ) # Get updated partner details updated_partner = await get_partner_details(ctx, partner_id) await ctx.info(f"Successfully updated partner {partner_id}") return updated_partner except Exception as e: await ctx.error(f"Error updating partner: {str(e)}") return {"error": str(e)} # ============================================================================ # CRM AUXILIARY TOOLS # ============================================================================ @mcp.tool() async def list_crm_stages(ctx: Context, team_id: Optional[int] = None) -> List[Dict[str, Any]]: """ List CRM stages for leads/opportunities. Args: team_id: Filter stages by team ID Returns: List of CRM stages """ try: odoo_client = await get_odoo_client_from_context(ctx) domain = [] if team_id: domain.append(("team_id", "=", team_id)) stages = await odoo_client.search_read( "crm.stage", domain, ["id", "name", "sequence", "fold", "team_id", "probability"], order="sequence asc" ) return [{ "id": stage["id"], "name": stage["name"], "sequence": stage.get("sequence", 0), "fold": stage.get("fold", False), "probability": stage.get("probability", 0.0), "team": { "id": stage["team_id"][0] if stage.get("team_id") else None, "name": stage["team_id"][1] if stage.get("team_id") else None } if stage.get("team_id") else None } for stage in stages] except Exception as e: await ctx.error(f"Error fetching CRM stages: {str(e)}") return {"error": str(e)} @mcp.tool() async def list_crm_teams(ctx: Context) -> List[Dict[str, Any]]: """ List CRM sales teams. Returns: List of CRM teams """ try: odoo_client = await get_odoo_client_from_context(ctx) teams = await odoo_client.search_read( "crm.team", [], ["id", "name", "user_id", "member_ids", "active"], order="name asc" ) return [{ "id": team["id"], "name": team["name"], "active": team.get("active", True), "leader": { "id": team["user_id"][0] if team.get("user_id") else None, "name": team["user_id"][1] if team.get("user_id") else None } if team.get("user_id") else None, "member_count": len(team.get("member_ids", [])) } for team in teams] except Exception as e: await ctx.error(f"Error fetching CRM teams: {str(e)}") return {"error": str(e)} @mcp.tool() async def get_lead_activities(ctx: Context, lead_id: int) -> List[Dict[str, Any]]: """ Get activities related to a lead/opportunity. Args: lead_id: Lead/Opportunity ID Returns: List of activities for the lead """ try: odoo_client = await get_odoo_client_from_context(ctx) activities = await odoo_client.search_read( "mail.activity", [("res_model", "=", "crm.lead"), ("res_id", "=", lead_id)], ["id", "activity_type_id", "summary", "date_deadline", "user_id", "state", "create_date"], order="date_deadline desc" ) return [{ "id": activity["id"], "type": { "id": activity["activity_type_id"][0] if activity.get("activity_type_id") else None, "name": activity["activity_type_id"][1] if activity.get("activity_type_id") else None } if activity.get("activity_type_id") else None, "summary": activity.get("summary", ""), "date_deadline": activity.get("date_deadline", ""), "state": activity.get("state", ""), "user": { "id": activity["user_id"][0] if activity.get("user_id") else None, "name": activity["user_id"][1] if activity.get("user_id") else None } if activity.get("user_id") else None, "create_date": activity.get("create_date", "") } for activity in activities] except Exception as e: await ctx.error(f"Error fetching lead activities: {str(e)}") return {"error": str(e)} @mcp.tool() async def get_academic_programs(ctx: Context, active_only: bool = True) -> List[Dict[str, Any]]: """ Get academic programs available in the system (ISEP specific). Args: active_only: Return only active programs Returns: List of academic programs """ try: odoo_client = await get_odoo_client_from_context(ctx) domain = [] if active_only: domain.append(("active", "=", True)) # Academic programs are stored as products in Odoo programs = await odoo_client.search_read( "product.template", domain, ["id", "name", "active", "list_price", "categ_id"], order="name asc" ) return [{ "id": program["id"], "name": program["name"], "active": program.get("active", True), "price": program.get("list_price", 0.0), "category": { "id": program["categ_id"][0] if program.get("categ_id") else None, "name": program["categ_id"][1] if program.get("categ_id") else None } if program.get("categ_id") else None } for program in programs] except Exception as e: await ctx.error(f"Error fetching academic programs: {str(e)}") return {"error": str(e)} @mcp.tool() async def get_crm_dashboard_stats(ctx: Context, team_id: Optional[int] = None, user_id: Optional[int] = None, date_from: Optional[str] = None, date_to: Optional[str] = None) -> Dict[str, Any]: """ Get CRM dashboard statistics. Args: team_id: Filter by team ID user_id: Filter by user ID date_from: Start date for statistics date_to: End date for statistics Returns: CRM dashboard statistics """ try: odoo_client = await get_odoo_client_from_context(ctx) # Build base domain domain = [] if team_id: domain.append(("team_id", "=", team_id)) if user_id: domain.append(("user_id", "=", user_id)) if date_from: domain.append(("create_date", ">=", date_from)) if date_to: domain.append(("create_date", "<=", date_to)) # Get leads count leads_domain = domain + [("type", "=", "lead")] leads_count = await odoo_client.execute_kw( "crm.lead", "search_count", [leads_domain] ) # Get opportunities count opps_domain = domain + [("type", "=", "opportunity")] opportunities_count = await odoo_client.execute_kw( "crm.lead", "search_count", [opps_domain] ) # Get won opportunities won_domain = domain + [("type", "=", "opportunity"), ("probability", "=", 100)] won_count = await odoo_client.execute_kw( "crm.lead", "search_count", [won_domain] ) # Get lost opportunities lost_domain = domain + [("type", "=", "opportunity"), ("probability", "=", 0), ("active", "=", False)] lost_count = await odoo_client.execute_kw( "crm.lead", "search_count", [lost_domain] ) # Get revenue data revenue_opps = await odoo_client.search_read( "crm.lead", domain + [("type", "=", "opportunity"), ("expected_revenue", ">", 0)], ["expected_revenue", "probability"] ) total_expected = sum(opp.get("expected_revenue", 0) for opp in revenue_opps) weighted_revenue = sum( opp.get("expected_revenue", 0) * (opp.get("probability", 0) / 100) for opp in revenue_opps ) return { "leads_count": leads_count, "opportunities_count": opportunities_count, "won_count": won_count, "lost_count": lost_count, "win_rate": round((won_count / max(opportunities_count, 1)) * 100, 2), "total_expected_revenue": round(total_expected, 2), "weighted_revenue": round(weighted_revenue, 2), "active_pipeline": opportunities_count - won_count - lost_count } except Exception as e: await ctx.error(f"Error getting CRM dashboard stats: {str(e)}") return {"error": str(e)}

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/iaisep/mcpserver'

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