Skip to main content
Glama
Jing-yilin

LinkedIn MCP Server

by Jing-yilin
index.ts42.1 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, CallToolResult, ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; import { HttpsProxyAgent } from 'https-proxy-agent'; import { encode } from '@toon-format/toon'; import * as fs from 'fs'; import * as path from 'path'; /** * Data cleaners for each endpoint type * Removes noise and keeps only useful fields for agents */ const DataCleaners = { cleanProfile(raw: any): any { if (!raw) return null; const exp = (raw.experience || []).map((e: any) => ({ position: e.position, company: e.companyName, location: e.location, duration: e.duration, startDate: e.startDate?.text, endDate: e.endDate?.text, description: e.description, })); const edu = (raw.education || []).map((e: any) => ({ school: e.schoolName || e.title, degree: e.degree, field: e.fieldOfStudy, period: e.period || `${e.startDate?.year || ''}-${e.endDate?.year || ''}`.replace(/^-|-$/g, ''), })); const skills = (raw.skills || []).map((s: any) => s.name); const certs = (raw.certifications || []).map((c: any) => ({ title: c.title, issuedBy: c.issuedBy, issuedAt: c.issuedAt, })); return { id: raw.id, publicIdentifier: raw.publicIdentifier, linkedinUrl: raw.linkedinUrl, name: `${raw.firstName || ''} ${raw.lastName || ''}`.trim(), headline: raw.headline, about: raw.about, location: raw.location?.linkedinText, photo: raw.photo || raw.profilePicture?.url, premium: raw.premium, influencer: raw.influencer, verified: raw.verified, openToWork: raw.openToWork, hiring: raw.hiring, connections: raw.connectionsCount, followers: raw.followerCount, experience: exp, education: edu, skills: skills, certifications: certs, }; }, cleanProfileSearchResult(raw: any): any { if (!raw) return null; return { id: raw.id, name: raw.name, position: raw.position, location: raw.location?.linkedinText, linkedinUrl: raw.linkedinUrl, publicIdentifier: raw.publicIdentifier, }; }, cleanCompany(raw: any): any { if (!raw) return null; const hq = (raw.locations || []).find((l: any) => l.headquarter) || raw.locations?.[0]; return { id: raw.id, universalName: raw.universalName, linkedinUrl: raw.linkedinUrl, name: raw.name, website: raw.website, logo: raw.logo, description: raw.description, employeeCount: raw.employeeCount, followers: raw.followerCount, headquarter: hq?.parsed?.text || hq?.city, }; }, cleanJob(raw: any): any { if (!raw) return null; return { id: raw.id, title: raw.title, linkedinUrl: raw.linkedinUrl || raw.url, state: raw.jobState, postedDate: raw.postedDate, location: raw.location?.linkedinText, company: raw.company?.name || raw.companyName, companyUrl: raw.company?.linkedinUrl || raw.companyLink, salary: raw.salary?.text || (raw.salary?.min && raw.salary?.max ? `${raw.salary.min}-${raw.salary.max} ${raw.salary.currency || ''}` : null), employmentType: raw.employmentType, workplaceType: raw.workplaceType, easyApply: raw.easyApply, description: raw.descriptionText, }; }, cleanJobSearchResult(raw: any): any { if (!raw) return null; return { id: raw.id, title: raw.title, url: raw.url, postedDate: raw.postedDate, company: raw.company?.name, location: raw.location?.linkedinText, easyApply: raw.easyApply, }; }, cleanPost(raw: any): any { if (!raw) return null; return { id: raw.id, linkedinUrl: raw.linkedinUrl, content: raw.content, authorName: raw.author?.name, authorType: raw.author?.type, postedAgo: raw.postedAt?.postedAgoText || raw.postedAt?.postedAgoShort, likes: raw.engagement?.likes, comments: raw.engagement?.comments, shares: raw.engagement?.shares, hasVideo: !!raw.postVideo, hasImages: (raw.postImages?.length || 0) > 0, }; }, cleanGroup(raw: any): any { if (!raw) return null; return { id: raw.id, linkedinUrl: raw.linkedinUrl, name: raw.name, members: raw.members || raw.memberCount, summary: raw.summary || raw.description, }; }, cleanGeoId(raw: any): any { if (!raw) return null; return { geoId: raw.geoId, title: raw.title, }; }, cleanComment(raw: any): any { if (!raw) return null; return { id: raw.id, content: raw.content, authorName: raw.author?.name, postedAgo: raw.postedAt?.postedAgoText, likes: raw.engagement?.likes, }; }, cleanReaction(raw: any): any { if (!raw) return null; return { name: raw.name, headline: raw.headline, reactionType: raw.reactionType, linkedinUrl: raw.linkedinUrl, }; }, }; /** * LinkedIn API MCP Server * Provides access to LinkedIn data through HarvestAPI service * Returns data in TOON format for token efficiency */ class LinkedInAPIMCPServer { private server: Server; private apiClient: AxiosInstance; private apiKey: string; constructor() { this.apiKey = process.env.HARVESTAPI_API_KEY || process.env.LINKEDIN_API_KEY || ''; this.server = new Server( { name: 'linkedin-mcp-server', version: '1.2.0', }, { capabilities: { tools: {}, }, } ); const axiosConfig: AxiosRequestConfig = { baseURL: 'https://api.harvest-api.com/linkedin', timeout: 30000, headers: { 'Content-Type': 'application/json', 'User-Agent': 'LinkedIn-MCP-Server/1.2.0' } }; const proxyUrl = process.env.PROXY_URL || process.env.HTTP_PROXY || process.env.HTTPS_PROXY; if (proxyUrl) { axiosConfig.httpsAgent = new HttpsProxyAgent(proxyUrl); axiosConfig.proxy = false; } this.apiClient = axios.create(axiosConfig); this.setupToolHandlers(); } private setupToolHandlers(): void { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'get_profile', description: 'Get LinkedIn profile information by URL, public identifier, or profile ID. Returns cleaned data in TOON format.', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'LinkedIn profile URL' }, publicIdentifier: { type: 'string', description: 'Public identifier (last part of LinkedIn URL)' }, profileId: { type: 'string', description: 'LinkedIn profile ID' }, findEmail: { type: 'boolean', description: 'Find email address for the profile', default: false }, includeAboutProfile: { type: 'boolean', description: 'Include detailed about section', default: false }, save_dir: { type: 'string', description: 'Directory to save cleaned JSON data' }, max_items: { type: 'integer', description: 'Maximum items in arrays (default: 5)', default: 5 }, }, required: [], }, } as Tool, { name: 'search_profiles', description: 'Search LinkedIn profiles by name, company, location. Returns cleaned data in TOON format.', inputSchema: { type: 'object', properties: { search: { type: 'string', description: 'Search profiles by name' }, currentCompany: { type: 'string', description: 'Filter by current company ID or URL' }, pastCompany: { type: 'string', description: 'Filter by past company ID or URL' }, school: { type: 'string', description: 'Filter by school ID or URL' }, firstName: { type: 'string', description: 'Filter by first name' }, lastName: { type: 'string', description: 'Filter by last name' }, title: { type: 'string', description: 'Filter by job title' }, location: { type: 'string', description: 'Filter by location text' }, geoId: { type: 'string', description: 'Filter by LinkedIn Geo ID' }, industryId: { type: 'string', description: 'Filter by industry ID' }, page: { type: 'integer', description: 'Page number', default: 1 }, save_dir: { type: 'string', description: 'Directory to save cleaned JSON data' }, max_items: { type: 'integer', description: 'Maximum results (default: 10)', default: 10 }, }, required: ['search'], }, } as Tool, { name: 'get_profile_posts', description: 'Get posts from a LinkedIn profile. Returns cleaned data in TOON format.', inputSchema: { type: 'object', properties: { profile: { type: 'string', description: 'LinkedIn profile URL' }, profileId: { type: 'string', description: 'LinkedIn profile ID (faster)' }, profilePublicIdentifier: { type: 'string', description: 'Profile public identifier' }, postedLimit: { type: 'string', description: 'Filter by time: 24h, week, month', enum: ['24h', 'week', 'month'] }, page: { type: 'integer', description: 'Page number', default: 1 }, paginationToken: { type: 'string', description: 'Pagination token' }, save_dir: { type: 'string', description: 'Directory to save cleaned JSON data' }, max_items: { type: 'integer', description: 'Maximum posts (default: 10)', default: 10 }, }, required: [], }, } as Tool, { name: 'get_profile_comments', description: 'Get comments made by a LinkedIn profile. Returns cleaned data in TOON format.', inputSchema: { type: 'object', properties: { profile: { type: 'string', description: 'LinkedIn profile URL' }, profileId: { type: 'string', description: 'LinkedIn profile ID (faster)' }, postedLimit: { type: 'string', description: 'Filter by time: 24h, week, month', enum: ['24h', 'week', 'month'] }, page: { type: 'integer', description: 'Page number', default: 1 }, paginationToken: { type: 'string', description: 'Pagination token' }, save_dir: { type: 'string', description: 'Directory to save cleaned JSON data' }, max_items: { type: 'integer', description: 'Maximum comments (default: 10)', default: 10 }, }, required: [], }, } as Tool, { name: 'get_profile_reactions', description: 'Get reactions from a LinkedIn profile. Returns cleaned data in TOON format.', inputSchema: { type: 'object', properties: { profile: { type: 'string', description: 'LinkedIn profile URL' }, profileId: { type: 'string', description: 'LinkedIn profile ID (faster)' }, page: { type: 'integer', description: 'Page number', default: 1 }, paginationToken: { type: 'string', description: 'Pagination token' }, save_dir: { type: 'string', description: 'Directory to save cleaned JSON data' }, max_items: { type: 'integer', description: 'Maximum reactions (default: 10)', default: 10 }, }, required: [], }, } as Tool, { name: 'get_company', description: 'Get LinkedIn company information. Returns cleaned data in TOON format.', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'LinkedIn company URL' }, universalName: { type: 'string', description: 'Company universal name (found in URL)' }, search: { type: 'string', description: 'Company name to search' }, save_dir: { type: 'string', description: 'Directory to save cleaned JSON data' }, }, required: [], }, } as Tool, { name: 'search_companies', description: 'Search LinkedIn companies. Returns cleaned data in TOON format.', inputSchema: { type: 'object', properties: { search: { type: 'string', description: 'Keywords to search' }, location: { type: 'string', description: 'Filter by location' }, geoId: { type: 'string', description: 'Filter by LinkedIn Geo ID' }, companySize: { type: 'string', description: 'Filter by size: 1-10, 11-50, 51-200, 201-500, 501-1000, 1001-5000, 5001-10000, 10001+' }, page: { type: 'integer', description: 'Page number', default: 1 }, save_dir: { type: 'string', description: 'Directory to save cleaned JSON data' }, max_items: { type: 'integer', description: 'Maximum results (default: 10)', default: 10 }, }, required: ['search'], }, } as Tool, { name: 'get_company_posts', description: 'Get posts from a LinkedIn company page. Returns cleaned data in TOON format.', inputSchema: { type: 'object', properties: { company: { type: 'string', description: 'LinkedIn company URL' }, companyId: { type: 'string', description: 'LinkedIn company ID (faster)' }, companyUniversalName: { type: 'string', description: 'Company universal name' }, postedLimit: { type: 'string', description: 'Filter by time: 24h, week, month', enum: ['24h', 'week', 'month'] }, page: { type: 'integer', description: 'Page number', default: 1 }, paginationToken: { type: 'string', description: 'Pagination token' }, save_dir: { type: 'string', description: 'Directory to save cleaned JSON data' }, max_items: { type: 'integer', description: 'Maximum posts (default: 10)', default: 10 }, }, required: [], }, } as Tool, { name: 'get_job', description: 'Get LinkedIn job details. Returns cleaned data in TOON format.', inputSchema: { type: 'object', properties: { jobId: { type: 'string', description: 'LinkedIn job ID' }, url: { type: 'string', description: 'LinkedIn job URL' }, save_dir: { type: 'string', description: 'Directory to save cleaned JSON data' }, }, required: [], }, } as Tool, { name: 'search_jobs', description: 'Search LinkedIn jobs. Returns cleaned data in TOON format.', inputSchema: { type: 'object', properties: { search: { type: 'string', description: 'Search jobs by title' }, companyId: { type: 'string', description: 'Filter by company ID' }, location: { type: 'string', description: 'Filter by location' }, geoId: { type: 'string', description: 'Filter by LinkedIn Geo ID' }, sortBy: { type: 'string', description: 'Sort by: relevance or date', enum: ['relevance', 'date'] }, workplaceType: { type: 'string', description: 'Filter: office, hybrid, remote', enum: ['office', 'hybrid', 'remote'] }, employmentType: { type: 'string', description: 'Filter: full-time, part-time, contract, temporary, volunteer, internship', enum: ['full-time', 'part-time', 'contract', 'temporary', 'volunteer', 'internship'] }, salary: { type: 'string', description: 'Filter by salary: 40k+, 60k+, 80k+, 100k+, 120k+, 140k+, 160k+, 180k+, 200k+' }, postedLimit: { type: 'string', description: 'Filter by post date: 24h, week, month', enum: ['24h', 'week', 'month'] }, experienceLevel: { type: 'string', description: 'Filter: internship, entry, associate, mid-senior, director, executive', enum: ['internship', 'entry', 'associate', 'mid-senior', 'director', 'executive'] }, industryId: { type: 'string', description: 'Filter by industry ID (comma-separated)' }, functionId: { type: 'string', description: 'Filter by job function ID (comma-separated)' }, under10Applicants: { type: 'boolean', description: 'Filter jobs with under 10 applicants' }, easyApply: { type: 'boolean', description: 'Filter Easy Apply jobs' }, page: { type: 'integer', description: 'Page number', default: 1 }, save_dir: { type: 'string', description: 'Directory to save cleaned JSON data' }, max_items: { type: 'integer', description: 'Maximum results (default: 10)', default: 10 }, }, required: [], }, } as Tool, { name: 'get_post', description: 'Get LinkedIn post details. Returns cleaned data in TOON format.', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'LinkedIn post URL (required)' }, save_dir: { type: 'string', description: 'Directory to save cleaned JSON data' }, }, required: ['url'], }, } as Tool, { name: 'search_posts', description: 'Search LinkedIn posts. Returns cleaned data in TOON format.', inputSchema: { type: 'object', properties: { search: { type: 'string', description: 'Keywords to search' }, profile: { type: 'string', description: 'Filter by author profile URL' }, profileId: { type: 'string', description: 'Filter by author profile ID' }, company: { type: 'string', description: 'Filter by company name' }, companyId: { type: 'string', description: 'Filter by company ID' }, authorsCompany: { type: 'string', description: 'Search posts from employees of a company' }, authorsCompanyId: { type: 'string', description: 'Filter by company ID of post authors' }, group: { type: 'string', description: 'Filter by group name' }, postedLimit: { type: 'string', description: 'Filter by time: 24h, week, month', enum: ['24h', 'week', 'month'] }, sortBy: { type: 'string', description: 'Sort by: relevance or date', enum: ['relevance', 'date'] }, page: { type: 'integer', description: 'Page number', default: 1 }, paginationToken: { type: 'string', description: 'Pagination token' }, save_dir: { type: 'string', description: 'Directory to save cleaned JSON data' }, max_items: { type: 'integer', description: 'Maximum results (default: 10)', default: 10 }, }, required: [], }, } as Tool, { name: 'get_post_comments', description: 'Get comments on a LinkedIn post. Returns cleaned data in TOON format.', inputSchema: { type: 'object', properties: { post: { type: 'string', description: 'LinkedIn post URL (required)' }, sortBy: { type: 'string', description: 'Sort by: relevance or date', enum: ['relevance', 'date'] }, page: { type: 'integer', description: 'Page number', default: 1 }, paginationToken: { type: 'string', description: 'Pagination token' }, save_dir: { type: 'string', description: 'Directory to save cleaned JSON data' }, max_items: { type: 'integer', description: 'Maximum comments (default: 10)', default: 10 }, }, required: ['post'], }, } as Tool, { name: 'get_post_reactions', description: 'Get reactions on a LinkedIn post. Returns cleaned data in TOON format.', inputSchema: { type: 'object', properties: { post: { type: 'string', description: 'LinkedIn post URL (required)' }, page: { type: 'integer', description: 'Page number', default: 1 }, save_dir: { type: 'string', description: 'Directory to save cleaned JSON data' }, max_items: { type: 'integer', description: 'Maximum reactions (default: 10)', default: 10 }, }, required: ['post'], }, } as Tool, { name: 'get_group', description: 'Get LinkedIn group information. Returns cleaned data in TOON format.', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'LinkedIn group URL' }, groupId: { type: 'string', description: 'LinkedIn group ID' }, save_dir: { type: 'string', description: 'Directory to save cleaned JSON data' }, }, required: [], }, } as Tool, { name: 'search_groups', description: 'Search LinkedIn groups. Returns cleaned data in TOON format.', inputSchema: { type: 'object', properties: { search: { type: 'string', description: 'Keywords to search' }, page: { type: 'integer', description: 'Page number', default: 1 }, save_dir: { type: 'string', description: 'Directory to save cleaned JSON data' }, max_items: { type: 'integer', description: 'Maximum results (default: 10)', default: 10 }, }, required: ['search'], }, } as Tool, { name: 'search_geo_id', description: 'Search LinkedIn Geo ID by location (for location-based filtering). Returns cleaned data in TOON format.', inputSchema: { type: 'object', properties: { search: { type: 'string', description: 'Location text to search' }, save_dir: { type: 'string', description: 'Directory to save cleaned JSON data' }, max_items: { type: 'integer', description: 'Maximum results (default: 10)', default: 10 }, }, required: ['search'], }, } as Tool, ], }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; if (!args) throw new McpError(ErrorCode.InvalidParams, 'Missing arguments'); switch (name) { case 'get_profile': return await this.getProfile(args as Record<string, any>); case 'search_profiles': return await this.searchProfiles(args as Record<string, any>); case 'get_profile_posts': return await this.getProfilePosts(args as Record<string, any>); case 'get_profile_comments': return await this.getProfileComments(args as Record<string, any>); case 'get_profile_reactions': return await this.getProfileReactions(args as Record<string, any>); case 'get_company': return await this.getCompany(args as Record<string, any>); case 'search_companies': return await this.searchCompanies(args as Record<string, any>); case 'get_company_posts': return await this.getCompanyPosts(args as Record<string, any>); case 'get_job': return await this.getJob(args as Record<string, any>); case 'search_jobs': return await this.searchJobs(args as Record<string, any>); case 'get_post': return await this.getPost(args as Record<string, any>); case 'search_posts': return await this.searchPosts(args as Record<string, any>); case 'get_post_comments': return await this.getPostComments(args as Record<string, any>); case 'get_post_reactions': return await this.getPostReactions(args as Record<string, any>); case 'get_group': return await this.getGroup(args as Record<string, any>); case 'search_groups': return await this.searchGroups(args as Record<string, any>); case 'search_geo_id': return await this.searchGeoId(args as Record<string, any>); default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { if (error instanceof McpError) throw error; const message = error instanceof Error ? error.message : 'Unknown error occurred'; throw new McpError(ErrorCode.InternalError, `HarvestAPI error: ${message}`); } }); } private async makeRequest(endpoint: string, params?: Record<string, any>): Promise<any> { try { const config: AxiosRequestConfig = { headers: {}, params: params || {}, }; if (this.apiKey && config.headers) { config.headers['X-API-Key'] = this.apiKey; } const response = await this.apiClient.get(endpoint, config); return response.data; } catch (error) { if (axios.isAxiosError(error)) { const statusCode = error.response?.status || 500; const errorMessage = error.response?.data?.message || error.response?.data?.error || error.message; throw new Error(`HarvestAPI error (${statusCode}): ${errorMessage}`); } throw error; } } private saveData(data: any, dir: string, toolName: string): string { try { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const filename = `${toolName}_${timestamp}.json`; const filepath = path.join(dir, filename); fs.writeFileSync(filepath, JSON.stringify(data, null, 2)); return filepath; } catch (e) { return `Error saving: ${e}`; } } private formatResponse( cleanedData: any, options: { saveDir?: string; toolName?: string; pagination?: any; } ): CallToolResult { const output: any = { data: cleanedData }; if (options.pagination) { output.pagination = { page: options.pagination.pageNumber, totalPages: options.pagination.totalPages, totalElements: options.pagination.totalElements, }; } let savedPath = ''; if (options.saveDir && options.toolName) { savedPath = this.saveData(output, options.saveDir, options.toolName); } const toonString = encode(output); let text = toonString; if (savedPath) { text += `\n\n[Cleaned data saved to: ${savedPath}]`; } return { content: [{ type: 'text', text }], }; } // Profile methods private async getProfile(args: Record<string, any>): Promise<CallToolResult> { const params: Record<string, any> = {}; if (args.url) params.url = args.url; if (args.publicIdentifier) params.publicIdentifier = args.publicIdentifier; if (args.profileId) params.profileId = args.profileId; if (args.findEmail) params.findEmail = args.findEmail; if (args.includeAboutProfile) params.includeAboutProfile = args.includeAboutProfile; if (!params.url && !params.publicIdentifier && !params.profileId) { throw new Error('At least one of url, publicIdentifier, or profileId is required'); } const data = await this.makeRequest('/profile', params); const maxItems = args.max_items || 5; const cleaned = DataCleaners.cleanProfile(data.element); if (cleaned) { if (cleaned.experience) cleaned.experience = cleaned.experience.slice(0, maxItems); if (cleaned.education) cleaned.education = cleaned.education.slice(0, maxItems); if (cleaned.skills) cleaned.skills = cleaned.skills.slice(0, maxItems); if (cleaned.certifications) cleaned.certifications = cleaned.certifications.slice(0, maxItems); } return this.formatResponse(cleaned, { saveDir: args.save_dir, toolName: 'get_profile', }); } private async searchProfiles(args: Record<string, any>): Promise<CallToolResult> { const params: Record<string, any> = { search: args.search }; if (args.currentCompany) params.currentCompany = args.currentCompany; if (args.pastCompany) params.pastCompany = args.pastCompany; if (args.school) params.school = args.school; if (args.firstName) params.firstName = args.firstName; if (args.lastName) params.lastName = args.lastName; if (args.title) params.title = args.title; if (args.location) params.location = args.location; if (args.geoId) params.geoId = args.geoId; if (args.industryId) params.industryId = args.industryId; if (args.page) params.page = args.page; const data = await this.makeRequest('/profile-search', params); const maxItems = args.max_items || 10; const cleaned = (data.elements || []).slice(0, maxItems).map(DataCleaners.cleanProfileSearchResult); return this.formatResponse(cleaned, { saveDir: args.save_dir, toolName: 'search_profiles', pagination: data.pagination, }); } private async getProfilePosts(args: Record<string, any>): Promise<CallToolResult> { const params: Record<string, any> = {}; if (args.profile) params.profile = args.profile; if (args.profileId) params.profileId = args.profileId; if (args.profilePublicIdentifier) params.profilePublicIdentifier = args.profilePublicIdentifier; if (args.postedLimit) params.postedLimit = args.postedLimit; if (args.page) params.page = args.page; if (args.paginationToken) params.paginationToken = args.paginationToken; if (!params.profile && !params.profileId && !params.profilePublicIdentifier) { throw new Error('At least one of profile, profileId, or profilePublicIdentifier is required'); } const data = await this.makeRequest('/profile-posts', params); const maxItems = args.max_items || 10; const cleaned = (data.elements || []).slice(0, maxItems).map(DataCleaners.cleanPost); return this.formatResponse(cleaned, { saveDir: args.save_dir, toolName: 'get_profile_posts', pagination: data.pagination, }); } private async getProfileComments(args: Record<string, any>): Promise<CallToolResult> { const params: Record<string, any> = {}; if (args.profile) params.profile = args.profile; if (args.profileId) params.profileId = args.profileId; if (args.postedLimit) params.postedLimit = args.postedLimit; if (args.page) params.page = args.page; if (args.paginationToken) params.paginationToken = args.paginationToken; if (!params.profile && !params.profileId) { throw new Error('At least one of profile or profileId is required'); } const data = await this.makeRequest('/profile-comments', params); const maxItems = args.max_items || 10; const cleaned = (data.elements || []).slice(0, maxItems).map(DataCleaners.cleanComment); return this.formatResponse(cleaned, { saveDir: args.save_dir, toolName: 'get_profile_comments', pagination: data.pagination, }); } private async getProfileReactions(args: Record<string, any>): Promise<CallToolResult> { const params: Record<string, any> = {}; if (args.profile) params.profile = args.profile; if (args.profileId) params.profileId = args.profileId; if (args.page) params.page = args.page; if (args.paginationToken) params.paginationToken = args.paginationToken; if (!params.profile && !params.profileId) { throw new Error('At least one of profile or profileId is required'); } const data = await this.makeRequest('/profile-reactions', params); const maxItems = args.max_items || 10; const cleaned = (data.elements || []).slice(0, maxItems).map(DataCleaners.cleanReaction); return this.formatResponse(cleaned, { saveDir: args.save_dir, toolName: 'get_profile_reactions', pagination: data.pagination, }); } // Company methods private async getCompany(args: Record<string, any>): Promise<CallToolResult> { const params: Record<string, any> = {}; if (args.url) params.url = args.url; if (args.universalName) params.universalName = args.universalName; if (args.search) params.search = args.search; if (!params.url && !params.universalName && !params.search) { throw new Error('At least one of url, universalName, or search is required'); } const data = await this.makeRequest('/company', params); const cleaned = DataCleaners.cleanCompany(data.element); return this.formatResponse(cleaned, { saveDir: args.save_dir, toolName: 'get_company', }); } private async searchCompanies(args: Record<string, any>): Promise<CallToolResult> { const params: Record<string, any> = { search: args.search }; if (args.location) params.location = args.location; if (args.geoId) params.geoId = args.geoId; if (args.companySize) params.companySize = args.companySize; if (args.page) params.page = args.page; const data = await this.makeRequest('/company-search', params); const maxItems = args.max_items || 10; const cleaned = (data.elements || []).slice(0, maxItems).map(DataCleaners.cleanCompany); return this.formatResponse(cleaned, { saveDir: args.save_dir, toolName: 'search_companies', pagination: data.pagination, }); } private async getCompanyPosts(args: Record<string, any>): Promise<CallToolResult> { const params: Record<string, any> = {}; if (args.company) params.company = args.company; if (args.companyId) params.companyId = args.companyId; if (args.companyUniversalName) params.companyUniversalName = args.companyUniversalName; if (args.postedLimit) params.postedLimit = args.postedLimit; if (args.page) params.page = args.page; if (args.paginationToken) params.paginationToken = args.paginationToken; if (!params.company && !params.companyId && !params.companyUniversalName) { throw new Error('At least one of company, companyId, or companyUniversalName is required'); } const data = await this.makeRequest('/company-posts', params); const maxItems = args.max_items || 10; const cleaned = (data.elements || []).slice(0, maxItems).map(DataCleaners.cleanPost); return this.formatResponse(cleaned, { saveDir: args.save_dir, toolName: 'get_company_posts', pagination: data.pagination, }); } // Job methods private async getJob(args: Record<string, any>): Promise<CallToolResult> { const params: Record<string, any> = {}; if (args.jobId) params.jobId = args.jobId; if (args.url) params.url = args.url; if (!params.jobId && !params.url) { throw new Error('At least one of jobId or url is required'); } const data = await this.makeRequest('/job', params); const cleaned = DataCleaners.cleanJob(data.element); return this.formatResponse(cleaned, { saveDir: args.save_dir, toolName: 'get_job', }); } private async searchJobs(args: Record<string, any>): Promise<CallToolResult> { const params: Record<string, any> = {}; if (args.search) params.search = args.search; if (args.companyId) params.companyId = args.companyId; if (args.location) params.location = args.location; if (args.geoId) params.geoId = args.geoId; if (args.sortBy) params.sortBy = args.sortBy; if (args.workplaceType) params.workplaceType = args.workplaceType; if (args.employmentType) params.employmentType = args.employmentType; if (args.salary) params.salary = args.salary; if (args.postedLimit) params.postedLimit = args.postedLimit; if (args.experienceLevel) params.experienceLevel = args.experienceLevel; if (args.industryId) params.industryId = args.industryId; if (args.functionId) params.functionId = args.functionId; if (args.under10Applicants) params.under10Applicants = args.under10Applicants; if (args.easyApply !== undefined) params.easyApply = args.easyApply; if (args.page) params.page = args.page; const data = await this.makeRequest('/job-search', params); const maxItems = args.max_items || 10; const cleaned = (data.elements || []).slice(0, maxItems).map(DataCleaners.cleanJobSearchResult); return this.formatResponse(cleaned, { saveDir: args.save_dir, toolName: 'search_jobs', pagination: data.pagination, }); } // Post methods private async getPost(args: Record<string, any>): Promise<CallToolResult> { const data = await this.makeRequest('/post', { url: args.url }); const cleaned = DataCleaners.cleanPost(data.element); return this.formatResponse(cleaned, { saveDir: args.save_dir, toolName: 'get_post', }); } private async searchPosts(args: Record<string, any>): Promise<CallToolResult> { const params: Record<string, any> = {}; if (args.search) params.search = args.search; if (args.profile) params.profile = args.profile; if (args.profileId) params.profileId = args.profileId; if (args.company) params.company = args.company; if (args.companyId) params.companyId = args.companyId; if (args.authorsCompany) params.authorsCompany = args.authorsCompany; if (args.authorsCompanyId) params.authorsCompanyId = args.authorsCompanyId; if (args.group) params.group = args.group; if (args.postedLimit) params.postedLimit = args.postedLimit; if (args.sortBy) params.sortBy = args.sortBy; if (args.page) params.page = args.page; if (args.paginationToken) params.paginationToken = args.paginationToken; const data = await this.makeRequest('/post-search', params); const maxItems = args.max_items || 10; const cleaned = (data.elements || []).slice(0, maxItems).map(DataCleaners.cleanPost); return this.formatResponse(cleaned, { saveDir: args.save_dir, toolName: 'search_posts', pagination: data.pagination, }); } private async getPostComments(args: Record<string, any>): Promise<CallToolResult> { const params: Record<string, any> = { post: args.post }; if (args.sortBy) params.sortBy = args.sortBy; if (args.page) params.page = args.page; if (args.paginationToken) params.paginationToken = args.paginationToken; const data = await this.makeRequest('/post-comments', params); const maxItems = args.max_items || 10; const cleaned = (data.elements || []).slice(0, maxItems).map(DataCleaners.cleanComment); return this.formatResponse(cleaned, { saveDir: args.save_dir, toolName: 'get_post_comments', pagination: data.pagination, }); } private async getPostReactions(args: Record<string, any>): Promise<CallToolResult> { const params: Record<string, any> = { post: args.post }; if (args.page) params.page = args.page; const data = await this.makeRequest('/post-reactions', params); const maxItems = args.max_items || 10; const cleaned = (data.elements || []).slice(0, maxItems).map(DataCleaners.cleanReaction); return this.formatResponse(cleaned, { saveDir: args.save_dir, toolName: 'get_post_reactions', pagination: data.pagination, }); } // Group methods private async getGroup(args: Record<string, any>): Promise<CallToolResult> { const params: Record<string, any> = {}; if (args.url) params.url = args.url; if (args.groupId) params.groupId = args.groupId; if (!params.url && !params.groupId) { throw new Error('At least one of url or groupId is required'); } const data = await this.makeRequest('/group', params); const cleaned = DataCleaners.cleanGroup(data.element); return this.formatResponse(cleaned, { saveDir: args.save_dir, toolName: 'get_group', }); } private async searchGroups(args: Record<string, any>): Promise<CallToolResult> { const params: Record<string, any> = { search: args.search }; if (args.page) params.page = args.page; const data = await this.makeRequest('/group-search', params); const maxItems = args.max_items || 10; const cleaned = (data.elements || []).slice(0, maxItems).map(DataCleaners.cleanGroup); return this.formatResponse(cleaned, { saveDir: args.save_dir, toolName: 'search_groups', pagination: data.pagination, }); } // Geo ID method private async searchGeoId(args: Record<string, any>): Promise<CallToolResult> { const data = await this.makeRequest('/geo-id-search', { search: args.search }); const maxItems = args.max_items || 10; const cleaned = (data.elements || []).slice(0, maxItems).map(DataCleaners.cleanGeoId); return this.formatResponse(cleaned, { saveDir: args.save_dir, toolName: 'search_geo_id', }); } async run(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); } } const server = new LinkedInAPIMCPServer(); server.run().catch(console.error);

Implementation Reference

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/Jing-yilin/linkedin-mcp-server'

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