/**
* 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: ''
})
};
}
};