Skip to main content
Glama
baptitse-jn

LinkedIn MCP Server

by baptitse-jn
linkedin-mcp.js26.2 kB
/** * LinkedIn MCP Server - Comprehensive Model Context Protocol implementation for LinkedIn * * This server provides tools and resources for LinkedIn integration including: * - Profile management and retrieval * - Post creation and management * - Company information * - Connection management * - Messaging capabilities * - Analytics and insights */ const https = require('https'); const url = require('url'); // Helper function to make HTTP requests function makeRequest(options, postData = null) { return new Promise((resolve, reject) => { const req = https.request(options, (res) => { let data = ''; res.on('data', (chunk) => data += chunk); res.on('end', () => { try { const result = { statusCode: res.statusCode, headers: res.headers, body: data }; resolve(result); } catch (error) { reject(error); } }); }); req.on('error', reject); if (postData) { req.write(postData); } req.end(); }); } // LinkedIn API Helper Class class LinkedInAPI { constructor(accessToken) { this.accessToken = accessToken; this.baseURL = 'https://api.linkedin.com/v2'; } async makeLinkedInRequest(endpoint, method = 'GET', data = null) { const parsedUrl = url.parse(`${this.baseURL}${endpoint}`); const options = { hostname: parsedUrl.hostname, port: 443, path: parsedUrl.path, method: method, headers: { 'Authorization': `Bearer ${this.accessToken}`, 'Content-Type': 'application/json', 'X-Restli-Protocol-Version': '2.0.0' } }; if (data && (method === 'POST' || method === 'PUT')) { const postData = JSON.stringify(data); options.headers['Content-Length'] = Buffer.byteLength(postData); return await makeRequest(options, postData); } return await makeRequest(options); } // Get current user's profile async getProfile() { return await this.makeLinkedInRequest('/people/~?projection=(id,firstName,lastName,headline,publicProfileUrl,profilePicture,location,industry,summary)'); } // Get profile by ID async getProfileById(personId) { return await this.makeLinkedInRequest(`/people/(id:${personId})?projection=(id,firstName,lastName,headline,publicProfileUrl,profilePicture,location,industry,summary)`); } // Share/post content async createPost(content, visibility = 'PUBLIC') { const postData = { author: `urn:li:person:${await this.getCurrentUserId()}`, lifecycleState: 'PUBLISHED', specificContent: { 'com.linkedin.ugc.ShareContent': { shareCommentary: { text: content }, shareMediaCategory: 'NONE' } }, visibility: { 'com.linkedin.ugc.MemberNetworkVisibility': visibility } }; return await this.makeLinkedInRequest('/ugcPosts', 'POST', postData); } // Get user's posts async getUserPosts(count = 50) { const userId = await this.getCurrentUserId(); return await this.makeLinkedInRequest(`/ugcPosts?q=authors&authors=List((urn:li:person:${userId}))&count=${count}&sortBy=CREATED`); } // Get company information async getCompany(companyId) { return await this.makeLinkedInRequest(`/organizations/${companyId}?projection=(id,name,description,website,industry,specialties,foundedOn,headquarters,logo)`); } // Search companies async searchCompanies(keywords, count = 10) { return await this.makeLinkedInRequest(`/organizationLookup?q=keywords&keywords=${encodeURIComponent(keywords)}&count=${count}`); } // Get connections async getConnections(start = 0, count = 50) { return await this.makeLinkedInRequest(`/connections?start=${start}&count=${count}&projection=(elements*(to~(id,firstName,lastName,headline)))`); } // Send connection request async sendConnectionRequest(personId, message = '') { const postData = { invitee: { 'com.linkedin.voyager.growth.invitation.InviteeProfile': { profileId: personId } }, message: message }; return await this.makeLinkedInRequest('/invitations', 'POST', postData); } // Get messages async getMessages(conversationId = null, count = 20) { if (conversationId) { return await this.makeLinkedInRequest(`/messaging/conversations/${conversationId}/events?count=${count}`); } return await this.makeLinkedInRequest(`/messaging/conversations?count=${count}`); } // Send message async sendMessage(recipients, message) { const postData = { recipients: recipients.map(id => `urn:li:person:${id}`), message: { body: message } }; return await this.makeLinkedInRequest('/messaging/conversations', 'POST', postData); } // Helper method to get current user ID async getCurrentUserId() { if (!this._currentUserId) { const profile = await this.getProfile(); const profileData = JSON.parse(profile.body); this._currentUserId = profileData.id; } return this._currentUserId; } } // Main handler exports.handler = async (event) => { // Only handle POST requests if (event.httpMethod !== 'POST') { return { statusCode: 405, body: 'Method Not Allowed' }; } try { const body = JSON.parse(event.body); const { method, params, id } = body; // MCP initialization if (method === 'mcp/init') { return { statusCode: 200, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 'Access-Control-Allow-Methods': 'POST, OPTIONS' }, body: JSON.stringify({ jsonrpc: '2.0', result: { server: { name: 'linkedin-mcp-server', version: '1.0.0', description: 'Complete LinkedIn integration MCP server for profile management, posting, messaging, and analytics' }, protocol: { version: '0.1', capabilities: { logging: {}, tools: {}, resources: {} } } }, id }) }; } // List available tools if (method === 'mcp/listTools') { return { statusCode: 200, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, body: JSON.stringify({ jsonrpc: '2.0', result: { tools: [ { name: 'get-profile', description: 'Get LinkedIn profile information for the authenticated user or a specific person', schema: { type: 'object', properties: { personId: { type: 'string', description: 'LinkedIn person ID (optional, defaults to current user)' } }, additionalProperties: false } }, { name: 'create-post', description: 'Create a new LinkedIn post', schema: { type: 'object', properties: { content: { type: 'string', description: 'Post content text' }, visibility: { type: 'string', enum: ['PUBLIC', 'CONNECTIONS'], description: 'Post visibility (default: PUBLIC)' } }, required: ['content'], additionalProperties: false } }, { name: 'get-posts', description: 'Get user\'s LinkedIn posts', schema: { type: 'object', properties: { count: { type: 'number', description: 'Number of posts to retrieve (default: 50, max: 100)' } }, additionalProperties: false } }, { name: 'get-company', description: 'Get company information by company ID', schema: { type: 'object', properties: { companyId: { type: 'string', description: 'LinkedIn company ID' } }, required: ['companyId'], additionalProperties: false } }, { name: 'search-companies', description: 'Search for companies by keywords', schema: { type: 'object', properties: { keywords: { type: 'string', description: 'Search keywords' }, count: { type: 'number', description: 'Number of results (default: 10, max: 50)' } }, required: ['keywords'], additionalProperties: false } }, { name: 'get-connections', description: 'Get user\'s LinkedIn connections', schema: { type: 'object', properties: { start: { type: 'number', description: 'Starting position (default: 0)' }, count: { type: 'number', description: 'Number of connections to retrieve (default: 50, max: 500)' } }, additionalProperties: false } }, { name: 'send-connection-request', description: 'Send a connection request to another LinkedIn user', schema: { type: 'object', properties: { personId: { type: 'string', description: 'LinkedIn person ID to connect with' }, message: { type: 'string', description: 'Optional connection message' } }, required: ['personId'], additionalProperties: false } }, { name: 'get-messages', description: 'Get LinkedIn messages/conversations', schema: { type: 'object', properties: { conversationId: { type: 'string', description: 'Specific conversation ID (optional)' }, count: { type: 'number', description: 'Number of messages/conversations to retrieve (default: 20, max: 100)' } }, additionalProperties: false } }, { name: 'send-message', description: 'Send a message to LinkedIn connections', schema: { type: 'object', properties: { recipients: { type: 'array', items: { type: 'string' }, description: 'Array of LinkedIn person IDs to send message to' }, message: { type: 'string', description: 'Message content' } }, required: ['recipients', 'message'], additionalProperties: false } }, { name: 'analyze-network', description: 'Analyze LinkedIn network and provide insights', schema: { type: 'object', properties: { analysisType: { type: 'string', enum: ['connections', 'posts', 'industry', 'activity'], description: 'Type of analysis to perform' } }, required: ['analysisType'], additionalProperties: false } } ] }, id }) }; } // Call a tool if (method === 'mcp/callTool') { const { name, args } = params; // Get access token from environment or args const accessToken = process.env.LINKEDIN_ACCESS_TOKEN || args?.accessToken; if (!accessToken) { return { statusCode: 400, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, body: JSON.stringify({ jsonrpc: '2.0', error: { code: -32602, message: 'LinkedIn access token is required. Set LINKEDIN_ACCESS_TOKEN environment variable or provide in args.accessToken' }, id }) }; } const linkedin = new LinkedInAPI(accessToken); try { let result = null; switch (name) { case 'get-profile': if (args?.personId) { result = await linkedin.getProfileById(args.personId); } else { result = await linkedin.getProfile(); } break; case 'create-post': result = await linkedin.createPost(args.content, args.visibility || 'PUBLIC'); break; case 'get-posts': result = await linkedin.getUserPosts(Math.min(args?.count || 50, 100)); break; case 'get-company': result = await linkedin.getCompany(args.companyId); break; case 'search-companies': result = await linkedin.searchCompanies(args.keywords, Math.min(args?.count || 10, 50)); break; case 'get-connections': result = await linkedin.getConnections(args?.start || 0, Math.min(args?.count || 50, 500)); break; case 'send-connection-request': result = await linkedin.sendConnectionRequest(args.personId, args.message || ''); break; case 'get-messages': result = await linkedin.getMessages(args?.conversationId, Math.min(args?.count || 20, 100)); break; case 'send-message': result = await linkedin.sendMessage(args.recipients, args.message); break; case 'analyze-network': // Perform network analysis based on type let analysisResult = {}; if (args.analysisType === 'connections') { const connections = await linkedin.getConnections(0, 500); const connectionData = JSON.parse(connections.body); analysisResult = { totalConnections: connectionData.paging?.total || 0, analysisType: 'connections', insights: [ 'Connection count indicates network size and reach', 'Diverse industry connections suggest broad professional network', 'Regular networking can help expand professional opportunities' ] }; } else if (args.analysisType === 'posts') { const posts = await linkedin.getUserPosts(50); const postData = JSON.parse(posts.body); analysisResult = { totalPosts: postData.elements?.length || 0, analysisType: 'posts', insights: [ 'Regular posting increases visibility and engagement', 'Content variety keeps audience interested', 'Engaging with others\' posts builds relationships' ] }; } else { analysisResult = { analysisType: args.analysisType, message: `Analysis type '${args.analysisType}' requires additional data collection` }; } result = { statusCode: 200, body: JSON.stringify(analysisResult) }; break; default: return { statusCode: 400, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, body: JSON.stringify({ jsonrpc: '2.0', error: { code: -32602, message: `Tool '${name}' not found` }, id }) }; } return { statusCode: 200, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, body: JSON.stringify({ jsonrpc: '2.0', result: { content: [ { type: 'text', text: result.body } ] }, id }) }; } catch (error) { return { statusCode: 500, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, body: JSON.stringify({ jsonrpc: '2.0', error: { code: -32603, message: `LinkedIn API error: ${error.message}` }, id }) }; } } // List resources if (method === 'mcp/listResources') { return { statusCode: 200, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, body: JSON.stringify({ jsonrpc: '2.0', result: { resources: [ { name: 'linkedin-api-guide', uri: 'docs://linkedin-api-guide', metadata: { mimeType: 'text/markdown' }, description: 'Comprehensive guide to LinkedIn API usage and best practices' }, { name: 'oauth-setup', uri: 'docs://oauth-setup', metadata: { mimeType: 'text/markdown' }, description: 'Step-by-step guide for setting up LinkedIn OAuth authentication' }, { name: 'api-limits', uri: 'docs://api-limits', metadata: { mimeType: 'text/markdown' }, description: 'LinkedIn API rate limits and usage guidelines' }, { name: 'best-practices', uri: 'docs://best-practices', metadata: { mimeType: 'text/markdown' }, description: 'Best practices for LinkedIn automation and API usage' }, { name: 'error-codes', uri: 'docs://error-codes', metadata: { mimeType: 'text/markdown' }, description: 'LinkedIn API error codes and troubleshooting guide' } ] }, id }) }; } // Read a resource if (method === 'mcp/readResource') { const { uri } = params; const resources = { 'docs://linkedin-api-guide': `# LinkedIn API Guide ## Overview This MCP server provides comprehensive LinkedIn integration capabilities including: - **Profile Management**: Get and update profile information - **Content Sharing**: Create and manage posts - **Company Data**: Search and retrieve company information - **Network Management**: Manage connections and send invitations - **Messaging**: Send and retrieve messages - **Analytics**: Analyze network and content performance ## Authentication All LinkedIn API calls require a valid access token. You can provide this in two ways: 1. Set the LINKEDIN_ACCESS_TOKEN environment variable 2. Include accessToken in the tool arguments ## Available Tools - get-profile: Get profile information - create-post: Share content on LinkedIn - get-posts: Retrieve user's posts - get-company: Get company information - search-companies: Search for companies - get-connections: Retrieve connections - send-connection-request: Send connection invitations - get-messages: Retrieve messages/conversations - send-message: Send messages to connections - analyze-network: Perform network analysis`, 'docs://oauth-setup': `# LinkedIn OAuth Setup ## Steps to Get Access Token 1. **Create LinkedIn App** - Go to https://developer.linkedin.com/ - Create a new application - Note your Client ID and Client Secret 2. **Configure Permissions** Required scopes for this MCP: - r_liteprofile (basic profile access) - r_emailaddress (email access) - w_member_social (post sharing) - r_organization_social (company data) - w_organization_social (company posting) 3. **OAuth Flow** - Redirect users to: https://www.linkedin.com/oauth/v2/authorization - Exchange code for access token at: https://www.linkedin.com/oauth/v2/accessToken 4. **Use Access Token** Set as environment variable: LINKEDIN_ACCESS_TOKEN=your_token_here`, 'docs://api-limits': `# LinkedIn API Limits ## Rate Limits LinkedIn enforces the following rate limits: - **Consumer API**: 500 requests per user per day - **Marketing API**: Varies by product (typically 1000-100k per day) - **Per-second limits**: Usually 10-100 requests per second ## Best Practices - Implement exponential backoff for rate limit errors - Cache responses when possible - Use batch operations when available - Monitor your usage via LinkedIn Developer portal ## Error Handling - 429 status code indicates rate limit exceeded - Check Retry-After header for retry timing - Different endpoints have different limits`, 'docs://best-practices': `# LinkedIn API Best Practices ## Content Guidelines - Keep posts professional and valuable - Avoid spam or excessive promotional content - Use proper formatting and hashtags - Include relevant visuals when possible ## Network Management - Personalize connection requests - Don't send too many requests at once - Respect users' privacy and preferences - Build genuine professional relationships ## Messaging - Keep messages relevant and professional - Don't send unsolicited promotional messages - Respect LinkedIn's messaging policies - Use templates sparingly and personalize ## Technical Best Practices - Always handle errors gracefully - Implement proper logging - Use pagination for large data sets - Keep access tokens secure`, 'docs://error-codes': `# LinkedIn API Error Codes ## Common HTTP Status Codes ### 400 Bad Request - Invalid parameters - Malformed request body - Missing required fields ### 401 Unauthorized - Invalid or expired access token - Insufficient permissions - Token revoked by user ### 403 Forbidden - API not available for your application - Exceeded rate limits for this resource - Access denied to specific resource ### 404 Not Found - Resource doesn't exist - Invalid endpoint URL - User not found ### 429 Too Many Requests - Rate limit exceeded - Check Retry-After header - Implement exponential backoff ### 500 Internal Server Error - LinkedIn server error - Retry after delay - Check LinkedIn status page ## Troubleshooting Tips 1. Verify access token validity 2. Check required permissions 3. Validate request parameters 4. Monitor rate limit usage 5. Review LinkedIn API documentation` }; if (resources[uri]) { return { statusCode: 200, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, body: JSON.stringify({ jsonrpc: '2.0', result: { contents: [ { uri: uri, text: resources[uri] } ] }, id }) }; } return { statusCode: 404, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, body: JSON.stringify({ jsonrpc: '2.0', error: { code: -32602, message: `Resource '${uri}' not found` }, id }) }; } // Method not found return { statusCode: 400, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, body: JSON.stringify({ jsonrpc: '2.0', error: { code: -32601, message: `Method '${method}' not found` }, id }) }; } catch (error) { console.error('Error processing request:', error); return { statusCode: 500, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, body: JSON.stringify({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error' }, id: '' }) }; } };

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/baptitse-jn/linkedin_mcp'

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