Skip to main content
Glama

Createve.AI Nexus

by spgoodman
api.pyβ€’41 kB
""" LinkedIn API implementation for the Createve.AI API Server. This module implements various LinkedIn API functionality using the unofficial linkedin-api package. """ import os import json import time import traceback from typing import Dict, List, Optional, Tuple, Union from linkedin_api import Linkedin from dotenv import load_dotenv import uuid from datetime import datetime, timedelta # Load environment variables (for API credentials) load_dotenv() class LinkedInClientManager: """Singleton manager for LinkedIn API client.""" _instance = None _client = None @classmethod def get_client(cls, email=None, password=None): """Get or create a LinkedIn client instance.""" if cls._client is None: # Try environment variables if not provided directly email = email or os.getenv("LINKEDIN_EMAIL") password = password or os.getenv("LINKEDIN_PASSWORD") if not email or not password: raise ValueError( "LinkedIn credentials not provided. Please set LINKEDIN_EMAIL and " "LINKEDIN_PASSWORD environment variables or provide them as parameters." ) cls._client = Linkedin(email, password) return cls._client class LinkedInProfileFetcher: """ Fetches comprehensive LinkedIn profile information. Retrieves detailed profile data including work experience, education, skills, and contact information. """ CATEGORY = "linkedin" @classmethod def INPUT_TYPES(cls): """Define input parameters for the profile fetcher.""" return { "required": { "profile_identifier": ("STRING", { "multiline": False, "placeholder": "Username or URN ID (e.g., 'john-doe' or 'urn:li:fs_miniProfile:AbC123')" }), }, "optional": { "include_contact_info": ("BOOLEAN", {"default": False}), "include_skills": ("BOOLEAN", {"default": True}), "use_urn": ("BOOLEAN", {"default": False}), "linkedin_email": ("STRING", {"multiline": False}), "linkedin_password": ("STRING", {"multiline": False}), } } RETURN_TYPES = ("DICT",) RETURN_NAMES = ("profile_data",) FUNCTION = "fetch_profile" def fetch_profile( self, profile_identifier: str, include_contact_info: bool = False, include_skills: bool = True, use_urn: bool = False, linkedin_email: Optional[str] = None, linkedin_password: Optional[str] = None ) -> Tuple[Dict]: """ Fetch a LinkedIn profile using either a public ID or URN ID. Args: profile_identifier: Username (from URL) or URN ID of the profile include_contact_info: Whether to fetch contact information include_skills: Whether to fetch skills information use_urn: Whether the profile_identifier is a URN instead of username linkedin_email: Optional email for LinkedIn login linkedin_password: Optional password for LinkedIn login Returns: Dictionary containing profile information """ try: # Get client client = LinkedInClientManager.get_client(linkedin_email, linkedin_password) # Determine how to fetch the profile if use_urn: profile = client.get_profile(urn_id=profile_identifier) else: profile = client.get_profile(public_id=profile_identifier) # Get additional information if requested if include_contact_info: try: contact_info = client.get_profile_contact_info( profile_identifier if use_urn else profile_identifier ) profile['contact_info'] = contact_info except Exception as e: profile['contact_info'] = {"error": str(e)} if include_skills: try: skills = client.get_profile_skills( profile_identifier if use_urn else profile_identifier ) profile['skills'] = skills except Exception as e: profile['skills'] = {"error": str(e)} # Add metadata profile['_metadata'] = { 'retrieved_at': time.strftime('%Y-%m-%d %H:%M:%S'), 'source': 'linkedin-api' } return (profile,) except Exception as e: error_details = { 'error': str(e), 'traceback': traceback.format_exc(), '_metadata': { 'retrieved_at': time.strftime('%Y-%m-%d %H:%M:%S'), 'source': 'linkedin-api' } } return (error_details,) class LinkedInPeopleSearch: """ Searches for people on LinkedIn based on various criteria. Provides comprehensive people search functionality with filters for keywords, companies, locations, and more. """ CATEGORY = "linkedin" @classmethod def INPUT_TYPES(cls): """Define input parameters for people search.""" return { "required": { "keywords": ("STRING", { "multiline": False, "placeholder": "Search keywords (e.g., 'Software Engineer')" }), }, "optional": { "location": ("STRING", {"multiline": False, "default": ""}), "company_name": ("STRING", {"multiline": False, "default": ""}), "school_name": ("STRING", {"multiline": False, "default": ""}), "network_depth": (["All", "1st", "2nd", "3rd"], {"default": "All"}), "max_results": ("INTEGER", {"default": 10, "min": 1, "max": 100}), "linkedin_email": ("STRING", {"multiline": False}), "linkedin_password": ("STRING", {"multiline": False}), } } RETURN_TYPES = ("DICT",) RETURN_NAMES = ("search_results",) FUNCTION = "search_people" def search_people( self, keywords: str, location: str = "", company_name: str = "", school_name: str = "", network_depth: str = "All", max_results: int = 10, linkedin_email: Optional[str] = None, linkedin_password: Optional[str] = None ) -> Tuple[Dict]: """ Search for people on LinkedIn based on provided criteria. Args: keywords: Search keywords or phrases location: Location filter company_name: Company name filter school_name: School name filter network_depth: Connection level (All, 1st, 2nd, 3rd) max_results: Maximum number of results to return linkedin_email: Optional email for LinkedIn login linkedin_password: Optional password for LinkedIn login Returns: Dictionary containing search results """ try: # Get client client = LinkedInClientManager.get_client(linkedin_email, linkedin_password) # Prepare search parameters search_params = {} if keywords: search_params["keywords"] = keywords if company_name: search_params["keyword_company"] = company_name if location: search_params["keyword_location"] = location if school_name: search_params["keyword_school"] = school_name # Map network depth network_map = { "1st": ["F"], "2nd": ["S"], "3rd": ["O"], "All": ["F", "S", "O"] } if network_depth in network_map: search_params["network_depths"] = network_map[network_depth] # Execute search results = client.search_people(**search_params) # Limit results results = results[:max_results] # Get full profiles for the results enriched_results = [] for result in results: public_id = result.get('public_id') if public_id: try: profile = client.get_profile(public_id=public_id) enriched_results.append(profile) except Exception as e: # If we can't get the full profile, include the basic one result['profile_error'] = str(e) enriched_results.append(result) else: enriched_results.append(result) response = { 'count': len(enriched_results), 'results': enriched_results, '_metadata': { 'retrieved_at': time.strftime('%Y-%m-%d %H:%M:%S'), 'source': 'linkedin-api', 'search_criteria': { 'keywords': keywords, 'location': location, 'company_name': company_name, 'school_name': school_name, 'network_depth': network_depth } } } return (response,) except Exception as e: error_details = { 'error': str(e), 'traceback': traceback.format_exc(), '_metadata': { 'retrieved_at': time.strftime('%Y-%m-%d %H:%M:%S'), 'source': 'linkedin-api' } } return (error_details,) class LinkedInJobSearch: """ Searches for job listings on LinkedIn based on various criteria. Provides comprehensive job search functionality with filters for keywords, location, experience level, and job types. """ CATEGORY = "linkedin" @classmethod def INPUT_TYPES(cls): """Define input parameters for job search.""" return { "required": { "keywords": ("STRING", { "multiline": False, "placeholder": "Job search keywords (e.g., 'Data Scientist')" }), }, "optional": { "location": ("STRING", {"multiline": False, "default": ""}), "experience_levels": (["All", "Internship", "Entry level", "Associate", "Mid-Senior", "Director", "Executive"], {"default": "All"}), "job_types": (["All", "Full-time", "Part-time", "Contract", "Temporary", "Volunteer", "Internship"], {"default": "All"}), "remote_options": (["All", "On-site", "Remote", "Hybrid"], {"default": "All"}), "max_results": ("INTEGER", {"default": 10, "min": 1, "max": 100}), "linkedin_email": ("STRING", {"multiline": False}), "linkedin_password": ("STRING", {"multiline": False}), } } RETURN_TYPES = ("DICT",) RETURN_NAMES = ("job_results",) FUNCTION = "search_jobs" def search_jobs( self, keywords: str, location: str = "", experience_levels: str = "All", job_types: str = "All", remote_options: str = "All", max_results: int = 10, linkedin_email: Optional[str] = None, linkedin_password: Optional[str] = None ) -> Tuple[Dict]: """ Search for jobs on LinkedIn based on provided criteria. Args: keywords: Search keywords or phrases location: Location filter experience_levels: Experience level filter job_types: Job type filter remote_options: Remote work options filter max_results: Maximum number of results to return linkedin_email: Optional email for LinkedIn login linkedin_password: Optional password for LinkedIn login Returns: Dictionary containing job search results """ try: # Get client client = LinkedInClientManager.get_client(linkedin_email, linkedin_password) # Prepare search parameters search_params = {} if keywords: search_params["keywords"] = keywords if location: search_params["location_name"] = location # Map experience levels experience_map = { "Internship": ["1"], "Entry level": ["2"], "Associate": ["3"], "Mid-Senior": ["4"], "Director": ["5"], "Executive": ["6"] } if experience_levels != "All": search_params["experience"] = experience_map.get(experience_levels, []) # Map job types job_type_map = { "Full-time": ["F"], "Part-time": ["P"], "Contract": ["C"], "Temporary": ["T"], "Volunteer": ["V"], "Internship": ["I"] } if job_types != "All": search_params["job_type"] = job_type_map.get(job_types, []) # Map remote options remote_map = { "On-site": ["1"], "Remote": ["2"], "Hybrid": ["3"] } if remote_options != "All": search_params["remote"] = remote_map.get(remote_options, []) # Execute search results = client.search_jobs(**search_params) # Limit results results = results[:max_results] # Get full job details for each result enriched_results = [] for job in results: job_id = job.get('entityUrn', '').split(':')[-1] if job_id: try: details = client.get_job(job_id) # Get skills if available try: skills = client.get_job_skills(job_id) details['skills'] = skills except: pass enriched_results.append(details) except Exception as e: # If we can't get the full details, include the basic one job['details_error'] = str(e) enriched_results.append(job) else: enriched_results.append(job) response = { 'count': len(enriched_results), 'results': enriched_results, '_metadata': { 'retrieved_at': time.strftime('%Y-%m-%d %H:%M:%S'), 'source': 'linkedin-api', 'search_criteria': { 'keywords': keywords, 'location': location, 'experience_levels': experience_levels, 'job_types': job_types, 'remote_options': remote_options } } } return (response,) except Exception as e: error_details = { 'error': str(e), 'traceback': traceback.format_exc(), '_metadata': { 'retrieved_at': time.strftime('%Y-%m-%d %H:%M:%S'), 'source': 'linkedin-api' } } return (error_details,) class LinkedInMessaging: """ Sends messages to LinkedIn connections. Enables sending messages to existing conversations or starting new ones. """ CATEGORY = "linkedin" @classmethod def INPUT_TYPES(cls): """Define input parameters for messaging.""" return { "required": { "message_body": ("STRING", { "multiline": True, "placeholder": "Your message text here..." }), }, "optional": { "conversation_urn_id": ("STRING", { "multiline": False, "placeholder": "Conversation URN ID for existing conversations" }), "recipient_profile_id": ("STRING", { "multiline": False, "placeholder": "Public ID of recipient (for new conversations)" }), "linkedin_email": ("STRING", {"multiline": False}), "linkedin_password": ("STRING", {"multiline": False}), } } RETURN_TYPES = ("DICT",) RETURN_NAMES = ("message_result",) FUNCTION = "send_message" def send_message( self, message_body: str, conversation_urn_id: Optional[str] = None, recipient_profile_id: Optional[str] = None, linkedin_email: Optional[str] = None, linkedin_password: Optional[str] = None ) -> Tuple[Dict]: """ Send a message on LinkedIn. Args: message_body: Text content of the message conversation_urn_id: ID for existing conversations recipient_profile_id: Public ID for new conversations linkedin_email: Optional email for LinkedIn login linkedin_password: Optional password for LinkedIn login Returns: Dictionary containing message status """ try: # Get client client = LinkedInClientManager.get_client(linkedin_email, linkedin_password) # Validate inputs if not conversation_urn_id and not recipient_profile_id: raise ValueError( "Either conversation_urn_id or recipient_profile_id must be provided" ) # Prepare parameters params = { "message_body": message_body } # Determine if we're messaging an existing conversation or creating a new one if conversation_urn_id: params["conversation_urn_id"] = conversation_urn_id message_type = "existing_conversation" else: # First get the profile to get the URN profile = client.get_profile(public_id=recipient_profile_id) if not profile: raise ValueError(f"Could not find profile with ID: {recipient_profile_id}") # Get the entity URN from the profile urn = profile.get('entityUrn') if not urn: raise ValueError("Could not find URN for the specified profile") params["recipients"] = [urn] message_type = "new_conversation" # Send the message success = client.send_message(**params) # API returns True on failure, False on success (counterintuitive) is_success = not success response = { 'success': is_success, 'message_type': message_type, 'recipient': recipient_profile_id or conversation_urn_id, '_metadata': { 'sent_at': time.strftime('%Y-%m-%d %H:%M:%S'), 'source': 'linkedin-api' } } return (response,) except Exception as e: error_details = { 'error': str(e), 'traceback': traceback.format_exc(), '_metadata': { 'retrieved_at': time.strftime('%Y-%m-%d %H:%M:%S'), 'source': 'linkedin-api' } } return (error_details,) class LinkedInCompanyInfo: """ Retrieves comprehensive information about companies on LinkedIn. Fetches company profiles including details, updates, and statistics. """ CATEGORY = "linkedin" @classmethod def INPUT_TYPES(cls): """Define input parameters for company info retrieval.""" return { "required": { "company_identifier": ("STRING", { "multiline": False, "placeholder": "Company name or ID (e.g., 'google')" }), }, "optional": { "include_updates": ("BOOLEAN", {"default": False}), "max_updates": ("INTEGER", {"default": 5, "min": 1, "max": 50}), "linkedin_email": ("STRING", {"multiline": False}), "linkedin_password": ("STRING", {"multiline": False}), } } RETURN_TYPES = ("DICT",) RETURN_NAMES = ("company_data",) FUNCTION = "get_company" def get_company( self, company_identifier: str, include_updates: bool = False, max_updates: int = 5, linkedin_email: Optional[str] = None, linkedin_password: Optional[str] = None ) -> Tuple[Dict]: """ Fetch information about a LinkedIn company. Args: company_identifier: Company name or ID include_updates: Whether to include company updates max_updates: Maximum number of updates to include linkedin_email: Optional email for LinkedIn login linkedin_password: Optional password for LinkedIn login Returns: Dictionary containing company information """ try: # Get client client = LinkedInClientManager.get_client(linkedin_email, linkedin_password) # Get company data company = client.get_company(company_identifier) # Get company updates if requested if include_updates: try: updates = client.get_company_updates( public_id=company_identifier, max_results=max_updates ) company['updates'] = updates except Exception as e: company['updates'] = {"error": str(e)} # Add metadata company['_metadata'] = { 'retrieved_at': time.strftime('%Y-%m-%d %H:%M:%S'), 'source': 'linkedin-api' } return (company,) except Exception as e: error_details = { 'error': str(e), 'traceback': traceback.format_exc(), '_metadata': { 'retrieved_at': time.strftime('%Y-%m-%d %H:%M:%S'), 'source': 'linkedin-api' } } return (error_details,) class LinkedInConnectionManager: """ Manages LinkedIn connections. Enables sending connection requests, accepting/declining invitations, and managing connection status. """ CATEGORY = "linkedin" @classmethod def INPUT_TYPES(cls): """Define input parameters for connection management.""" return { "required": { "action_type": (["add_connection", "get_pending_invitations"], { "default": "add_connection" }), "profile_identifier": ("STRING", { "multiline": False, "placeholder": "Username or URN ID (e.g., 'john-doe' or 'urn:li:fs_miniProfile:AbC123')" }), }, "optional": { "message": ("STRING", { "multiline": True, "default": "I'd like to connect with you on LinkedIn.", "placeholder": "Custom message for connection request..." }), "use_urn": ("BOOLEAN", {"default": False}), "track_status": ("BOOLEAN", {"default": False}), "linkedin_email": ("STRING", {"multiline": False}), "linkedin_password": ("STRING", {"multiline": False}), } } RETURN_TYPES = ("DICT",) RETURN_NAMES = ("connection_result",) FUNCTION = "manage_connection" def manage_connection( self, action_type: str, profile_identifier: str, message: str = "I'd like to connect with you on LinkedIn.", use_urn: bool = False, track_status: bool = False, linkedin_email: Optional[str] = None, linkedin_password: Optional[str] = None ) -> Tuple[Dict]: """ Manage LinkedIn connections. Args: action_type: The type of action to perform profile_identifier: Username or URN ID of the profile message: Custom message for connection request use_urn: Whether the profile_identifier is a URN instead of username track_status: Whether to track the status of the request linkedin_email: Optional email for LinkedIn login linkedin_password: Optional password for LinkedIn login Returns: Dictionary containing operation result """ try: # Get client client = LinkedInClientManager.get_client(linkedin_email, linkedin_password) # Prepare response response = { 'action_type': action_type, 'profile_identifier': profile_identifier, 'success': False, '_metadata': { 'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'), 'source': 'linkedin-api' } } # Handle different actions if action_type == "add_connection": # Determine if we're using a public ID or URN if use_urn: # For URN, we can use it directly profile_urn = profile_identifier else: # For public ID, we need to get the profile first to get the URN profile = client.get_profile(public_id=profile_identifier) if not profile: raise ValueError(f"Could not find profile with ID: {profile_identifier}") profile_urn = profile.get('entityUrn') if not profile_urn: raise ValueError("Could not find URN for the specified profile") # Send the connection request result = client.add_connection(profile_urn, message=message) # Store request details if tracking is enabled if track_status: request_id = f"conn-{uuid.uuid4()}" response['request_id'] = request_id # In a real implementation, you might store this in a database response['success'] = result response['message_sent'] = message if result else None elif action_type == "get_pending_invitations": # Get pending connection invitations invitations = client.get_invitations() response['invitations'] = invitations response['count'] = len(invitations) response['success'] = True return (response,) except Exception as e: error_details = { 'error': str(e), 'traceback': traceback.format_exc(), 'action_type': action_type, 'profile_identifier': profile_identifier, '_metadata': { 'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'), 'source': 'linkedin-api' } } return (error_details,) class LinkedInConversationFetcher: """ Retrieves LinkedIn messages and conversations. Fetches message threads, individual messages, and conversation history. """ CATEGORY = "linkedin" @classmethod def INPUT_TYPES(cls): """Define input parameters for conversation fetching.""" return { "required": { "fetch_type": (["all_conversations", "conversation_details"], { "default": "all_conversations" }), }, "optional": { "conversation_urn_id": ("STRING", { "multiline": False, "placeholder": "Conversation URN ID (required for conversation_details)" }), "max_conversations": ("INTEGER", {"default": 10, "min": 1, "max": 100}), "include_message_body": ("BOOLEAN", {"default": True}), "linkedin_email": ("STRING", {"multiline": False}), "linkedin_password": ("STRING", {"multiline": False}), } } RETURN_TYPES = ("DICT",) RETURN_NAMES = ("conversation_data",) FUNCTION = "fetch_conversations" def fetch_conversations( self, fetch_type: str, conversation_urn_id: Optional[str] = None, max_conversations: int = 10, include_message_body: bool = True, linkedin_email: Optional[str] = None, linkedin_password: Optional[str] = None ) -> Tuple[Dict]: """ Fetch LinkedIn conversations and messages. Args: fetch_type: Type of fetch operation to perform conversation_urn_id: URN ID of specific conversation (for conversation_details) max_conversations: Maximum number of conversations to return include_message_body: Whether to include message content linkedin_email: Optional email for LinkedIn login linkedin_password: Optional password for LinkedIn login Returns: Dictionary containing conversation data """ try: # Get client client = LinkedInClientManager.get_client(linkedin_email, linkedin_password) # Prepare response response = { 'fetch_type': fetch_type, '_metadata': { 'retrieved_at': time.strftime('%Y-%m-%d %H:%M:%S'), 'source': 'linkedin-api' } } # Handle different fetch types if fetch_type == "all_conversations": # Get list of all conversations conversations = client.get_conversations() # Limit to max requested conversations = conversations[:max_conversations] # Include message body if requested if include_message_body: for conversation in conversations: conversation_id = conversation.get('entityUrn', '').split(':')[-1] if conversation_id: try: messages = client.get_conversation(conversation_id) conversation['messages'] = messages except Exception as e: conversation['messages'] = {"error": str(e)} response['conversations'] = conversations response['count'] = len(conversations) elif fetch_type == "conversation_details": # Validate input if not conversation_urn_id: raise ValueError("conversation_urn_id is required for conversation_details") # Get conversation details conversation_id = conversation_urn_id.split(':')[-1] if ':' in conversation_urn_id else conversation_urn_id messages = client.get_conversation(conversation_id) response['conversation_id'] = conversation_id response['messages'] = messages response['count'] = len(messages) if isinstance(messages, list) else 0 return (response,) except Exception as e: error_details = { 'error': str(e), 'traceback': traceback.format_exc(), 'fetch_type': fetch_type, '_metadata': { 'retrieved_at': time.strftime('%Y-%m-%d %H:%M:%S'), 'source': 'linkedin-api' } } return (error_details,) class LinkedInProfileViewTracker: """ Tracks and analyzes LinkedIn profile views. Retrieves information about who viewed your profile and provides analytics. """ CATEGORY = "linkedin" @classmethod def INPUT_TYPES(cls): """Define input parameters for profile view tracking.""" return { "required": { "track_type": (["viewers", "analytics", "visit_profile"], { "default": "viewers" }), }, "optional": { "target_profile_id": ("STRING", { "multiline": False, "placeholder": "Username of profile to visit (for visit_profile)" }), "time_range": (["day", "week", "month"], {"default": "week"}), "max_results": ("INTEGER", {"default": 20, "min": 1, "max": 100}), "linkedin_email": ("STRING", {"multiline": False}), "linkedin_password": ("STRING", {"multiline": False}), } } RETURN_TYPES = ("DICT",) RETURN_NAMES = ("tracking_data",) FUNCTION = "track_profile_views" def track_profile_views( self, track_type: str, target_profile_id: Optional[str] = None, time_range: str = "week", max_results: int = 20, linkedin_email: Optional[str] = None, linkedin_password: Optional[str] = None ) -> Tuple[Dict]: """ Track and analyze LinkedIn profile views. Args: track_type: Type of tracking operation to perform target_profile_id: Profile ID to visit (for visit_profile) time_range: Time range for analytics (day, week, month) max_results: Maximum number of results to return linkedin_email: Optional email for LinkedIn login linkedin_password: Optional password for LinkedIn login Returns: Dictionary containing tracking data """ try: # Get client client = LinkedInClientManager.get_client(linkedin_email, linkedin_password) # Prepare response response = { 'track_type': track_type, 'time_range': time_range, '_metadata': { 'retrieved_at': time.strftime('%Y-%m-%d %H:%M:%S'), 'source': 'linkedin-api' } } # Handle different tracking types if track_type == "viewers": # Get profile viewers viewers = client.get_profile_views() # Limit to max requested viewers = viewers[:max_results] if isinstance(viewers, list) else viewers # Enrich viewer profiles with additional info if isinstance(viewers, list): enriched_viewers = [] for viewer in viewers: public_id = viewer.get('publicIdentifier') if public_id: try: profile = client.get_profile(public_id=public_id) viewer['profile'] = profile enriched_viewers.append(viewer) except Exception as e: viewer['profile_error'] = str(e) enriched_viewers.append(viewer) else: enriched_viewers.append(viewer) viewers = enriched_viewers response['viewers'] = viewers response['count'] = len(viewers) if isinstance(viewers, list) else 0 elif track_type == "analytics": # Get profile view analytics (in a real implementation, this might # involve aggregating and analyzing the raw viewer data) # Calculate time period for analytics if time_range == "day": period = timedelta(days=1) elif time_range == "month": period = timedelta(days=30) else: # week period = timedelta(days=7) start_date = datetime.now() - period # In this implementation, we'll simulate analytics with some detailed metrics # In a real implementation, you would use client.get_profile_views() and analyze the data analytics = { 'total_views': 0, # Would be calculated from actual data 'unique_viewers': 0, # Would be calculated from actual data 'view_trends': { 'daily': [], # Would contain daily view counts 'by_industry': {}, # Would contain views broken down by viewer industry 'by_company': {}, # Would contain views broken down by viewer company }, 'engagement_metrics': { 'connection_requests': 0, 'message_responses': 0, 'content_engagement': 0, }, 'period': { 'start': start_date.strftime('%Y-%m-%d'), 'end': datetime.now().strftime('%Y-%m-%d'), 'range': time_range } } response['analytics'] = analytics elif track_type == "visit_profile": # Validate input if not target_profile_id: raise ValueError("target_profile_id is required for visit_profile") # Visit the profile to register a view # Note: The LinkedIn API doesn't have a direct "visit profile" method # But getting a profile simulates visiting it profile = client.get_profile(public_id=target_profile_id) response['visited_profile'] = { 'profile_id': target_profile_id, 'success': bool(profile), 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') } return (response,) except Exception as e: error_details = { 'error': str(e), 'traceback': traceback.format_exc(), 'track_type': track_type, '_metadata': { 'retrieved_at': time.strftime('%Y-%m-%d %H:%M:%S'), 'source': 'linkedin-api' } } return (error_details,)

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/spgoodman/createveai-nexus-server'

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