@ldraney/mcp-linkedin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/package.json +50 -0
- package/src/auth-server.js +223 -0
- package/src/database.js +288 -0
- package/src/index.js +654 -0
- package/src/linkedin-api.js +631 -0
- package/src/scheduler.js +201 -0
- package/src/schemas.js +629 -0
- package/src/tools.js +853 -0
- package/src/types.js +575 -0
package/src/scheduler.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Background scheduler daemon for LinkedIn posts
|
|
5
|
+
* Checks for due posts every minute and publishes them
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
require('dotenv').config();
|
|
9
|
+
const cron = require('node-cron');
|
|
10
|
+
const { getDatabase } = require('./database');
|
|
11
|
+
const LinkedInAPI = require('./linkedin-api');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {import('./types').ScheduledPost} ScheduledPost
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const MAX_RETRIES = 3;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get LinkedIn API client instance
|
|
21
|
+
* @returns {LinkedInAPI}
|
|
22
|
+
*/
|
|
23
|
+
function getAPIClient() {
|
|
24
|
+
return new LinkedInAPI({
|
|
25
|
+
accessToken: process.env.LINKEDIN_ACCESS_TOKEN,
|
|
26
|
+
apiVersion: process.env.LINKEDIN_API_VERSION,
|
|
27
|
+
personId: process.env.LINKEDIN_PERSON_ID
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Publish a scheduled post to LinkedIn
|
|
33
|
+
* @param {ScheduledPost} scheduledPost - The post to publish
|
|
34
|
+
* @returns {Promise<{success: boolean, postUrn?: string, error?: string}>}
|
|
35
|
+
*/
|
|
36
|
+
async function publishPost(scheduledPost) {
|
|
37
|
+
try {
|
|
38
|
+
const api = getAPIClient();
|
|
39
|
+
|
|
40
|
+
const postData = {
|
|
41
|
+
author: `urn:li:person:${process.env.LINKEDIN_PERSON_ID}`,
|
|
42
|
+
commentary: scheduledPost.commentary,
|
|
43
|
+
visibility: scheduledPost.visibility,
|
|
44
|
+
distribution: {
|
|
45
|
+
feedDistribution: 'MAIN_FEED'
|
|
46
|
+
},
|
|
47
|
+
lifecycleState: 'PUBLISHED'
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// If URL is provided, create a link post
|
|
51
|
+
if (scheduledPost.url) {
|
|
52
|
+
postData.content = {
|
|
53
|
+
article: {
|
|
54
|
+
source: scheduledPost.url
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const result = await api.createPost(postData);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
success: true,
|
|
63
|
+
postUrn: result.postUrn
|
|
64
|
+
};
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
error: error.message || 'Unknown error during publish'
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Process all due posts
|
|
75
|
+
* @returns {Promise<{published: number, failed: number}>}
|
|
76
|
+
*/
|
|
77
|
+
async function processDuePosts() {
|
|
78
|
+
const db = getDatabase();
|
|
79
|
+
const duePosts = db.getDuePosts();
|
|
80
|
+
|
|
81
|
+
let published = 0;
|
|
82
|
+
let failed = 0;
|
|
83
|
+
|
|
84
|
+
for (const post of duePosts) {
|
|
85
|
+
console.log(`[${new Date().toISOString()}] Processing scheduled post: ${post.id}`);
|
|
86
|
+
console.log(` Commentary: ${post.commentary.substring(0, 50)}...`);
|
|
87
|
+
|
|
88
|
+
const result = await publishPost(post);
|
|
89
|
+
|
|
90
|
+
if (result.success) {
|
|
91
|
+
db.markAsPublished(post.id, result.postUrn);
|
|
92
|
+
console.log(` ✓ Published successfully: ${result.postUrn}`);
|
|
93
|
+
published++;
|
|
94
|
+
} else {
|
|
95
|
+
db.markAsFailed(post.id, result.error);
|
|
96
|
+
console.log(` ✗ Failed: ${result.error} (attempt ${post.retryCount + 1})`);
|
|
97
|
+
|
|
98
|
+
// Check if we should retry
|
|
99
|
+
if (post.retryCount + 1 < MAX_RETRIES) {
|
|
100
|
+
console.log(` Will retry on next cycle`);
|
|
101
|
+
// Reset to pending for retry
|
|
102
|
+
db.resetForRetry(post.id);
|
|
103
|
+
} else {
|
|
104
|
+
console.log(` Max retries reached, marked as failed`);
|
|
105
|
+
}
|
|
106
|
+
failed++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { published, failed };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Start the scheduler
|
|
115
|
+
* @param {string} [cronExpression='* * * * *'] - Cron expression (default: every minute)
|
|
116
|
+
*/
|
|
117
|
+
function startScheduler(cronExpression = '* * * * *') {
|
|
118
|
+
console.log(`[${new Date().toISOString()}] LinkedIn Post Scheduler Started`);
|
|
119
|
+
console.log(` Cron expression: ${cronExpression}`);
|
|
120
|
+
console.log(` Access token configured: ${!!process.env.LINKEDIN_ACCESS_TOKEN}`);
|
|
121
|
+
console.log(` Person ID: ${process.env.LINKEDIN_PERSON_ID}`);
|
|
122
|
+
console.log('');
|
|
123
|
+
|
|
124
|
+
// Initial check on startup
|
|
125
|
+
console.log('Running initial check...');
|
|
126
|
+
processDuePosts().then(({ published, failed }) => {
|
|
127
|
+
if (published > 0 || failed > 0) {
|
|
128
|
+
console.log(`Initial check: ${published} published, ${failed} failed`);
|
|
129
|
+
} else {
|
|
130
|
+
console.log('No posts due at startup');
|
|
131
|
+
}
|
|
132
|
+
console.log('');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Schedule regular checks
|
|
136
|
+
const task = cron.schedule(cronExpression, async () => {
|
|
137
|
+
const timestamp = new Date().toISOString();
|
|
138
|
+
const db = getDatabase();
|
|
139
|
+
const pendingCount = db.getScheduledPosts('pending', 100).length;
|
|
140
|
+
|
|
141
|
+
if (pendingCount > 0) {
|
|
142
|
+
console.log(`[${timestamp}] Checking for due posts (${pendingCount} pending)...`);
|
|
143
|
+
const { published, failed } = await processDuePosts();
|
|
144
|
+
if (published > 0 || failed > 0) {
|
|
145
|
+
console.log(` Results: ${published} published, ${failed} failed`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Handle graceful shutdown
|
|
151
|
+
process.on('SIGINT', () => {
|
|
152
|
+
console.log('\nShutting down scheduler...');
|
|
153
|
+
task.stop();
|
|
154
|
+
const db = getDatabase();
|
|
155
|
+
db.close();
|
|
156
|
+
process.exit(0);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
process.on('SIGTERM', () => {
|
|
160
|
+
console.log('\nShutting down scheduler...');
|
|
161
|
+
task.stop();
|
|
162
|
+
const db = getDatabase();
|
|
163
|
+
db.close();
|
|
164
|
+
process.exit(0);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return task;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* CLI entry point
|
|
172
|
+
*/
|
|
173
|
+
function main() {
|
|
174
|
+
// Check required environment variables
|
|
175
|
+
if (!process.env.LINKEDIN_ACCESS_TOKEN) {
|
|
176
|
+
console.error('Error: LINKEDIN_ACCESS_TOKEN not set in environment');
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!process.env.LINKEDIN_PERSON_ID) {
|
|
181
|
+
console.error('Error: LINKEDIN_PERSON_ID not set in environment');
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Allow custom cron expression via CLI argument
|
|
186
|
+
const cronExpression = process.argv[2] || '* * * * *';
|
|
187
|
+
|
|
188
|
+
startScheduler(cronExpression);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Export for testing
|
|
192
|
+
module.exports = {
|
|
193
|
+
publishPost,
|
|
194
|
+
processDuePosts,
|
|
195
|
+
startScheduler
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Run if called directly
|
|
199
|
+
if (require.main === module) {
|
|
200
|
+
main();
|
|
201
|
+
}
|