@maheidem/linkedin-mcp
Version:
Comprehensive LinkedIn API MCP server with automatic Claude configuration
705 lines • 25 kB
JavaScript
"use strict";
/**
* Comprehensive LinkedIn API Client
* Implements ALL LinkedIn API endpoints and authentication flows
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LinkedInAPIClient = void 0;
const crypto_1 = __importDefault(require("crypto"));
const url_1 = require("url");
class LinkedInAPIClient {
config;
accessToken;
refreshToken;
tokenExpiry;
rateLimits = new Map();
// API category clients
people;
organizations;
content;
socialActions;
marketing;
talent;
learning;
messaging;
events;
salesNavigator;
compliance;
constructor(config) {
this.config = {
...config,
baseUrl: config.baseUrl || 'https://api.linkedin.com/rest',
legacyBaseUrl: config.legacyBaseUrl || 'https://api.linkedin.com/v2'
};
// Initialize API category clients
this.people = new PeopleAPI(this);
this.organizations = new OrganizationsAPI(this);
this.content = new ContentAPI(this);
this.socialActions = new SocialActionsAPI(this);
this.marketing = new MarketingAPI(this);
this.talent = new TalentAPI(this);
this.learning = new LearningAPI(this);
this.messaging = new MessagingAPI(this);
this.events = new EventsAPI(this);
this.salesNavigator = new SalesNavigatorAPI(this);
this.compliance = new ComplianceAPI(this);
}
// OAuth 2.0 Authentication Methods
async getAuthorizationUrl(params) {
const state = params.state || crypto_1.default.randomUUID();
const scopes = params.scopes.join(' ');
const authParams = {
response_type: 'code',
client_id: this.config.clientId,
redirect_uri: this.config.redirectUri,
state,
scope: scopes
};
let codeChallenge;
let codeVerifier;
if (params.usePKCE) {
codeVerifier = crypto_1.default.randomBytes(32).toString('base64url');
codeChallenge = crypto_1.default.createHash('sha256').update(codeVerifier).digest('base64url');
authParams.code_challenge = codeChallenge;
authParams.code_challenge_method = 'S256';
}
const queryString = new url_1.URLSearchParams(authParams).toString();
const authUrl = `https://www.linkedin.com/oauth/v2/authorization?${queryString}`;
return {
authorizationUrl: authUrl,
state,
codeVerifier,
expires: Date.now() + (30 * 60 * 1000) // 30 minutes
};
}
async exchangeCodeForToken(params) {
const tokenParams = new url_1.URLSearchParams({
grant_type: 'authorization_code',
code: params.code,
redirect_uri: this.config.redirectUri,
client_id: this.config.clientId,
client_secret: this.config.clientSecret
});
if (params.codeVerifier) {
tokenParams.append('code_verifier', params.codeVerifier);
}
const response = await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json'
},
body: tokenParams
});
if (!response.ok) {
throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
}
const tokenData = await response.json();
this.accessToken = tokenData.access_token;
this.refreshToken = tokenData.refresh_token;
this.tokenExpiry = Date.now() + (tokenData.expires_in * 1000);
return {
access_token: tokenData.access_token,
expires_in: tokenData.expires_in,
refresh_token: tokenData.refresh_token,
scope: tokenData.scope,
token_type: 'Bearer',
expires_at: this.tokenExpiry
};
}
async refreshAccessToken(params) {
const tokenParams = new url_1.URLSearchParams({
grant_type: 'refresh_token',
refresh_token: params.refreshToken,
client_id: this.config.clientId,
client_secret: this.config.clientSecret
});
const response = await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json'
},
body: tokenParams
});
if (!response.ok) {
throw new Error(`Token refresh failed: ${response.status} ${response.statusText}`);
}
const tokenData = await response.json();
this.accessToken = tokenData.access_token;
this.tokenExpiry = Date.now() + (tokenData.expires_in * 1000);
return {
access_token: tokenData.access_token,
expires_in: tokenData.expires_in,
token_type: 'Bearer',
expires_at: this.tokenExpiry
};
}
// Core HTTP client with rate limiting and error handling
async makeRequest(endpoint, options = {}) {
const { method = 'GET', data, headers = {}, useV2 = false, apiCategory = 'general' } = options;
// Check rate limits
await this.checkRateLimit(apiCategory);
// Ensure token is valid
if (this.tokenExpiry && Date.now() >= this.tokenExpiry) {
throw new Error('Access token expired. Please refresh or re-authenticate.');
}
const baseUrl = useV2 ? this.config.legacyBaseUrl : this.config.baseUrl;
const url = `${baseUrl}${endpoint}`;
const requestHeaders = {
'Authorization': `Bearer ${this.accessToken}`,
'Accept': 'application/json',
'X-Restli-Protocol-Version': '2.0.0',
'User-Agent': 'linkedin-api-mcp/1.0.0',
...headers
};
if (!useV2) {
requestHeaders['LinkedIn-Version'] = this.config.apiVersion;
}
if (data && (method === 'POST' || method === 'PUT')) {
requestHeaders['Content-Type'] = 'application/json';
}
const requestOptions = {
method,
headers: requestHeaders,
body: data ? JSON.stringify(data) : undefined
};
const response = await fetch(url, requestOptions);
// Update rate limit info from headers
this.updateRateLimitInfo(apiCategory, response);
if (!response.ok) {
const errorBody = await response.text().catch(() => 'Unknown error');
throw new Error(`LinkedIn API error: ${response.status} ${response.statusText} - ${errorBody}`);
}
// Handle empty responses (like DELETE operations)
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
return { success: true, status: response.status };
}
return await response.json();
}
async checkRateLimit(apiCategory) {
const rateLimitInfo = this.rateLimits.get(apiCategory);
if (rateLimitInfo && rateLimitInfo.remaining <= 0) {
const resetTime = rateLimitInfo.reset * 1000;
const waitTime = resetTime - Date.now();
if (waitTime > 0) {
throw new Error(`Rate limit exceeded for ${apiCategory}. Resets in ${Math.ceil(waitTime / 1000)} seconds.`);
}
}
}
updateRateLimitInfo(apiCategory, response) {
const remaining = response.headers.get('X-RateLimit-Remaining');
const reset = response.headers.get('X-RateLimit-Reset');
const limit = response.headers.get('X-RateLimit-Limit');
if (remaining && reset && limit) {
this.rateLimits.set(apiCategory, {
remaining: parseInt(remaining),
reset: parseInt(reset),
limit: parseInt(limit)
});
}
}
// Utility methods
async getRateLimits(params = {}) {
if (params.apiCategory) {
const rateLimitInfo = this.rateLimits.get(params.apiCategory);
return rateLimitInfo || { message: 'No rate limit info available for this category' };
}
const allLimits = {};
for (const [category, info] of this.rateLimits.entries()) {
allLimits[category] = info;
}
return {
rateLimits: allLimits,
totalCategories: this.rateLimits.size
};
}
async getApiHealth() {
try {
// Simple health check - try to get the authenticated user's profile
const response = await this.makeRequest('/people/~', {
method: 'GET',
useV2: true
});
return {
status: 'healthy',
timestamp: new Date().toISOString(),
authenticated: !!this.accessToken,
tokenExpiry: this.tokenExpiry ? new Date(this.tokenExpiry).toISOString() : null
};
}
catch (error) {
return {
status: 'unhealthy',
timestamp: new Date().toISOString(),
error: error instanceof Error ? error.message : 'Unknown error',
authenticated: !!this.accessToken
};
}
}
// Setter methods for authentication
setAccessToken(token, expiresIn) {
this.accessToken = token;
if (expiresIn) {
this.tokenExpiry = Date.now() + (expiresIn * 1000);
}
}
setRefreshToken(token) {
this.refreshToken = token;
}
}
exports.LinkedInAPIClient = LinkedInAPIClient;
// API Category Classes
class PeopleAPI {
client;
constructor(client) {
this.client = client;
}
async getProfile(params = {}) {
const personId = params.personId || '~';
const fields = params.fields ? params.fields.join(',') : 'id,firstName,lastName,headline,summary';
return await this.client.makeRequest(`/people/${personId}?fields=${fields}`, {
method: 'GET',
useV2: true,
apiCategory: 'people'
});
}
async searchMembers(params = {}) {
const queryParams = new url_1.URLSearchParams();
queryParams.append('q', 'members');
if (params.keywords)
queryParams.append('keywords', params.keywords);
if (params.start)
queryParams.append('start', params.start.toString());
if (params.count)
queryParams.append('count', params.count.toString());
return await this.client.makeRequest(`/people?${queryParams.toString()}`, {
method: 'GET',
useV2: true,
apiCategory: 'people'
});
}
}
class OrganizationsAPI {
client;
constructor(client) {
this.client = client;
}
async getCompany(params) {
const fields = params.fields ? params.fields.join(',') : 'id,name,description,industry,website';
return await this.client.makeRequest(`/organizations/${params.organizationId}?fields=${fields}`, {
method: 'GET',
useV2: true,
apiCategory: 'organizations'
});
}
async getFollowerStats(params) {
const queryParams = new url_1.URLSearchParams();
queryParams.append('q', 'organizationalEntity');
queryParams.append('organizationalEntity', `urn:li:organization:${params.organizationId}`);
return await this.client.makeRequest(`/organizationalEntityFollowerStatistics?${queryParams.toString()}`, {
method: 'GET',
useV2: true,
apiCategory: 'organizations'
});
}
}
class ContentAPI {
client;
constructor(client) {
this.client = client;
}
async createPost(params) {
const postData = {
author: params.author,
commentary: params.text,
visibility: params.visibility || 'PUBLIC',
distribution: {
feedDistribution: 'MAIN_FEED',
targetEntities: [],
thirdPartyDistributionChannels: []
}
};
// Handle different post types
switch (params.postType) {
case 'TEXT':
// Text-only post, no additional content needed
break;
case 'IMAGE':
case 'VIDEO':
if (params.media) {
postData.content = {
media: {
title: params.text?.substring(0, 100) || '',
id: params.media[0]?.url || ''
}
};
}
break;
case 'ARTICLE':
if (params.articleLink) {
postData.content = {
article: {
source: params.articleLink,
title: params.text?.substring(0, 100) || '',
description: params.text || ''
}
};
}
break;
case 'POLL':
if (params.pollOptions) {
postData.content = {
poll: {
question: params.text || '',
options: params.pollOptions.map(option => ({ text: option }))
}
};
}
break;
}
return await this.client.makeRequest('/posts', {
method: 'POST',
data: postData,
apiCategory: 'content'
});
}
async getPosts(params = {}) {
if (params.postId) {
return await this.client.makeRequest(`/posts/${params.postId}`, {
method: 'GET',
apiCategory: 'content'
});
}
const queryParams = new url_1.URLSearchParams();
if (params.author)
queryParams.append('author', params.author);
if (params.start)
queryParams.append('start', params.start.toString());
if (params.count)
queryParams.append('count', params.count.toString());
const query = queryParams.toString();
const endpoint = query ? `/posts?${query}` : '/posts';
return await this.client.makeRequest(endpoint, {
method: 'GET',
apiCategory: 'content'
});
}
async updatePost(params) {
const updateData = {};
if (params.text)
updateData.commentary = params.text;
if (params.visibility)
updateData.visibility = params.visibility;
return await this.client.makeRequest(`/posts/${params.postId}`, {
method: 'PUT',
data: updateData,
apiCategory: 'content'
});
}
async deletePost(params) {
return await this.client.makeRequest(`/posts/${params.postId}`, {
method: 'DELETE',
apiCategory: 'content'
});
}
async createUGCPost(params) {
const ugcData = {
author: params.author,
lifecycleState: 'PUBLISHED',
specificContent: {
'com.linkedin.ugc.ShareContent': {
shareCommentary: {
text: params.text || ''
},
shareMediaCategory: params.media ? 'IMAGE' : 'NONE',
media: params.media || []
}
},
visibility: params.visibility || {
'com.linkedin.ugc.MemberNetworkVisibility': 'PUBLIC'
}
};
return await this.client.makeRequest('/ugcPosts', {
method: 'POST',
data: ugcData,
useV2: true,
apiCategory: 'content'
});
}
async getUploadUrl(params) {
const registerData = {
recipes: [`urn:li:digitalmediaRecipe:feedshare-${params.mediaType.toLowerCase()}`],
owner: params.owner,
serviceRelationships: [
{
relationshipType: 'OWNER',
identifier: 'urn:li:userGeneratedContent'
}
]
};
return await this.client.makeRequest('/assets?action=registerUpload', {
method: 'POST',
data: registerData,
useV2: true,
apiCategory: 'content'
});
}
async uploadAsset(params) {
// This would require file system access and multipart upload implementation
// For now, return a placeholder response
return {
message: 'Asset upload requires file system access',
uploadUrl: params.uploadUrl,
mediaType: params.mediaType,
filePath: params.filePath,
status: 'pending_implementation'
};
}
}
// Additional API classes would follow the same pattern...
class SocialActionsAPI {
client;
constructor(client) {
this.client = client;
}
async like(params) {
const likeData = {
actor: params.actor,
object: params.targetUrn
};
return await this.client.makeRequest('/socialActions/likes', {
method: 'POST',
data: likeData,
useV2: true,
apiCategory: 'social'
});
}
async comment(params) {
const commentData = {
actor: params.actor,
object: params.targetUrn,
message: {
text: params.text
}
};
return await this.client.makeRequest('/socialActions/comments', {
method: 'POST',
data: commentData,
useV2: true,
apiCategory: 'social'
});
}
async share(params) {
const shareData = {
actor: params.actor,
object: params.targetUrn,
commentary: params.commentary || ''
};
return await this.client.makeRequest('/socialActions/shares', {
method: 'POST',
data: shareData,
useV2: true,
apiCategory: 'social'
});
}
}
class MarketingAPI {
client;
constructor(client) {
this.client = client;
}
async createCampaign(params) {
return await this.client.makeRequest('/adCampaigns', {
method: 'POST',
data: params,
apiCategory: 'marketing'
});
}
async getCampaigns(params = {}) {
const queryParams = new url_1.URLSearchParams();
if (params.account)
queryParams.append('account', params.account);
if (params.status)
queryParams.append('status', params.status);
const query = queryParams.toString();
const endpoint = query ? `/adCampaigns?${query}` : '/adCampaigns';
return await this.client.makeRequest(endpoint, {
method: 'GET',
apiCategory: 'marketing'
});
}
async updateCampaign(params) {
const { campaignId, ...updateData } = params;
return await this.client.makeRequest(`/adCampaigns/${campaignId}`, {
method: 'PUT',
data: updateData,
apiCategory: 'marketing'
});
}
async getAnalytics(params) {
const queryParams = new url_1.URLSearchParams();
queryParams.append('campaigns', params.campaigns.join(','));
if (params.dateRange) {
queryParams.append('dateRange.start.day', params.dateRange.start.day.toString());
queryParams.append('dateRange.start.month', params.dateRange.start.month.toString());
queryParams.append('dateRange.start.year', params.dateRange.start.year.toString());
queryParams.append('dateRange.end.day', params.dateRange.end.day.toString());
queryParams.append('dateRange.end.month', params.dateRange.end.month.toString());
queryParams.append('dateRange.end.year', params.dateRange.end.year.toString());
}
if (params.fields)
queryParams.append('fields', params.fields.join(','));
return await this.client.makeRequest(`/adAnalytics?${queryParams.toString()}`, {
method: 'GET',
apiCategory: 'marketing'
});
}
async getTargetingFacets(params) {
const queryParams = new url_1.URLSearchParams();
queryParams.append('facetType', params.facetType);
if (params.locale)
queryParams.append('locale', params.locale);
return await this.client.makeRequest(`/adTargetingFacets?${queryParams.toString()}`, {
method: 'GET',
apiCategory: 'marketing'
});
}
}
// Placeholder classes for remaining APIs
class TalentAPI {
client;
constructor(client) {
this.client = client;
}
async postJob(params) {
return await this.client.makeRequest('/jobs', {
method: 'POST',
data: params,
apiCategory: 'talent'
});
}
async searchJobs(params) {
const queryParams = new url_1.URLSearchParams();
if (params.keywords)
queryParams.append('keywords', params.keywords);
return await this.client.makeRequest(`/jobs?${queryParams.toString()}`, {
method: 'GET',
apiCategory: 'talent'
});
}
async unifiedSearch(params) {
return await this.client.makeRequest('/talent/unifiedSearch', {
method: 'POST',
data: params,
apiCategory: 'talent'
});
}
}
class LearningAPI {
client;
constructor(client) {
this.client = client;
}
async getCourses(params) {
const queryParams = new url_1.URLSearchParams();
if (params.q)
queryParams.append('q', params.q);
if (params.keywords)
queryParams.append('keywords', params.keywords);
return await this.client.makeRequest(`/learningAssets?${queryParams.toString()}`, {
method: 'GET',
useV2: true,
apiCategory: 'learning'
});
}
async getClassifications(params) {
return await this.client.makeRequest('/learningClassifications', {
method: 'GET',
useV2: true,
apiCategory: 'learning'
});
}
}
class MessagingAPI {
client;
constructor(client) {
this.client = client;
}
async sendMessage(params) {
return await this.client.makeRequest('/messages', {
method: 'POST',
data: params,
useV2: true,
apiCategory: 'messaging'
});
}
async getConversations(params) {
const queryParams = new url_1.URLSearchParams();
if (params.participantId)
queryParams.append('participantId', params.participantId);
return await this.client.makeRequest(`/conversations?${queryParams.toString()}`, {
method: 'GET',
useV2: true,
apiCategory: 'messaging'
});
}
}
class EventsAPI {
client;
constructor(client) {
this.client = client;
}
async createEvent(params) {
return await this.client.makeRequest('/events', {
method: 'POST',
data: params,
useV2: true,
apiCategory: 'events'
});
}
async getEvents(params) {
const queryParams = new url_1.URLSearchParams();
if (params.organizer)
queryParams.append('organizer', params.organizer);
return await this.client.makeRequest(`/events?${queryParams.toString()}`, {
method: 'GET',
useV2: true,
apiCategory: 'events'
});
}
}
class SalesNavigatorAPI {
client;
constructor(client) {
this.client = client;
}
async profileAssociation(params) {
return await this.client.makeRequest('/salesNavigatorProfileAssociation', {
method: 'POST',
data: params,
apiCategory: 'sales'
});
}
}
class ComplianceAPI {
client;
constructor(client) {
this.client = client;
}
async getEvents(params) {
const queryParams = new url_1.URLSearchParams();
if (params.eventType)
queryParams.append('eventType', params.eventType);
return await this.client.makeRequest(`/complianceEvents?${queryParams.toString()}`, {
method: 'GET',
useV2: true,
apiCategory: 'compliance'
});
}
}
//# sourceMappingURL=linkedin-client.js.map