@ldraney/mcp-linkedin 0.1.2 → 0.2.1

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.
@@ -3,7 +3,8 @@
3
3
  "allow": [
4
4
  "WebSearch",
5
5
  "Bash(npm test:*)",
6
- "Bash(git push)"
6
+ "Bash(git push)",
7
+ "Bash(gh issue close:*)"
7
8
  ]
8
9
  },
9
10
  "enableAllProjectMcpServers": true,
package/.mcpbignore ADDED
@@ -0,0 +1,33 @@
1
+ # Development/test files
2
+ __tests__/
3
+ *.test.js
4
+ jest.config.js
5
+ .env
6
+ .env.example
7
+
8
+ # Documentation
9
+ *.md
10
+ !README.md
11
+
12
+ # Git
13
+ .git/
14
+ .gitignore
15
+
16
+ # IDE
17
+ .vscode/
18
+ .idea/
19
+
20
+ # Claude dev files
21
+ .claude/
22
+
23
+ # OAuth relay (separate deployment)
24
+ oauth-relay/
25
+
26
+ # Old database files
27
+ *.db
28
+ scheduled_posts.db
29
+
30
+ # Other
31
+ test_post.js
32
+ todos.db
33
+ *.html
package/README.md CHANGED
@@ -1,135 +1,115 @@
1
1
  # mcp-linkedin
2
2
 
3
- MCP server for managing LinkedIn posts - create, update, and manage posts to share expertise and GitHub projects.
3
+ MCP server for managing LinkedIn posts - create text, image, video, document, poll, and multi-image posts. Schedule posts, add comments and reactions.
4
4
 
5
- ## Overview
5
+ ## Installation
6
6
 
7
- This MCP (Model Context Protocol) server enables programmatic management of LinkedIn posts. The primary use case is to help share technical expertise, showcase GitHub projects, and engage with a professional audience through LinkedIn's platform.
7
+ ### MCPB Bundle (One-Click)
8
+ Download from [latest release](https://github.com/intelligent-staffing-systems/mcp-linkedin/releases/latest) and open `mcp-linkedin.mcpb` with Claude Desktop.
8
9
 
9
- ## User Story
10
-
11
- As a technical professional, I want to:
12
- - Create LinkedIn posts that link to my GitHub projects
13
- - Update existing posts to reflect project changes
14
- - Manage my LinkedIn content programmatically
15
- - Share my expertise and educational content with my professional network
16
-
17
- This tool is designed to help educate and demonstrate expertise to a LinkedIn audience through consistent, high-quality content sharing.
18
-
19
- ## Status
20
-
21
- ✅ **MVP Complete** - All core tools implemented and tested!
22
-
23
- **Milestones:**
24
- - ✅ LinkedIn Developer App with OAuth 2.0
25
- - ✅ 7 MCP tools implemented (create, get, delete posts + auth)
26
- - ✅ 24 automated tests passing
27
- - ✅ Type-safe with JSDoc + Zod validation
28
- - ✅ MCP server with stdio transport
29
- - ✅ Ready for Claude Desktop integration
30
-
31
- ## Features
32
-
33
- ### ✅ Implemented (MVP)
34
-
35
- - **linkedin_create_post** - Create simple text posts with hashtags and mentions
36
- - **linkedin_create_post_with_link** - Create posts with link previews (GitHub, blogs, etc.)
37
- - **linkedin_get_my_posts** - Retrieve your recent posts with pagination
38
- - **linkedin_delete_post** - Delete posts by URN
39
- - **linkedin_get_auth_url** - Generate OAuth authorization URL
40
- - **linkedin_exchange_code** - Exchange auth code for access token
41
- - **linkedin_get_user_info** - Get your LinkedIn profile information
42
-
43
- ### 🚧 Planned (Next Phase)
44
-
45
- - **Post scheduling** - Schedule posts for future publication (HIGH PRIORITY)
46
- - **Draft management** - Save and manage draft posts locally
47
- - **Image uploads** - Create posts with images
48
- - **Post updates** - Edit existing posts
49
- - **Analytics** - View post engagement metrics (if API supports)
50
-
51
- ## LinkedIn API
52
-
53
- This MCP will utilize LinkedIn's REST API:
54
- - Base URL: `https://api.linkedin.com/rest/posts`
55
- - Authentication: OAuth 2.0
56
- - Required permissions: `w_member_social` scope
57
- - API Documentation: [LinkedIn Posts API](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/posts-api)
58
-
59
- ## Development Philosophy
60
-
61
- 1. Start with clear user stories based on LinkedIn API capabilities
62
- 2. Refine requirements through discussion and iteration
63
- 3. Build incrementally with well-defined issues and branches
64
- 4. Test thoroughly with real-world use cases
65
-
66
- ## Getting Started
67
-
68
- ### Prerequisites
69
-
70
- 1. Create a LinkedIn Developer App at https://www.linkedin.com/developers/apps
71
- 2. Add these products to your app:
72
- - "Sign In with LinkedIn using OpenID Connect"
73
- - "Share on LinkedIn"
74
- 3. Configure OAuth redirect URI (e.g., `https://localhost:3000/callback`)
75
-
76
- ### Setup
77
-
78
- 1. Clone the repository:
10
+ ### npm / npx
79
11
  ```bash
80
- git clone https://github.com/intelligent-staffing-systems/mcp-linkedin.git
81
- cd mcp-linkedin
12
+ npx @ldraney/mcp-linkedin
82
13
  ```
83
14
 
84
- 2. Install dependencies:
85
- ```bash
86
- npm install
15
+ ### Manual MCP Config
16
+ ```json
17
+ {
18
+ "mcpServers": {
19
+ "linkedin": {
20
+ "command": "npx",
21
+ "args": ["@ldraney/mcp-linkedin"],
22
+ "env": {
23
+ "LINKEDIN_CLIENT_ID": "your-client-id",
24
+ "LINKEDIN_CLIENT_SECRET": "your-client-secret",
25
+ "LINKEDIN_REDIRECT_URI": "https://localhost:8888/callback",
26
+ "LINKEDIN_PERSON_ID": "your-person-id",
27
+ "LINKEDIN_ACCESS_TOKEN": "your-access-token"
28
+ }
29
+ }
30
+ }
31
+ }
87
32
  ```
88
33
 
89
- 3. Create `.env` file (copy from `.env.example` and fill in your values):
90
- ```bash
91
- cp .env.example .env
92
- ```
34
+ ## Tools (20 Total)
35
+
36
+ ### Content Creation
37
+ | Tool | Description |
38
+ |------|-------------|
39
+ | `linkedin_create_post` | Create text posts |
40
+ | `linkedin_create_post_with_link` | Posts with article/link preview |
41
+ | `linkedin_create_post_with_image` | Upload image + create post |
42
+ | `linkedin_create_post_with_video` | Upload video + create post |
43
+ | `linkedin_create_post_with_document` | Upload PDF/PPT/DOC + create post |
44
+ | `linkedin_create_post_with_multi_images` | Upload 2-20 images + create post |
45
+ | `linkedin_create_poll` | Create poll posts (2-4 options) |
46
+
47
+ ### Content Management
48
+ | Tool | Description |
49
+ |------|-------------|
50
+ | `linkedin_get_my_posts` | Retrieve recent posts (paginated) |
51
+ | `linkedin_update_post` | Edit existing posts |
52
+ | `linkedin_delete_post` | Delete by URN |
53
+
54
+ ### Social Interactions
55
+ | Tool | Description |
56
+ |------|-------------|
57
+ | `linkedin_add_comment` | Add comment to a post |
58
+ | `linkedin_add_reaction` | React to a post (LIKE, PRAISE, etc.) |
59
+
60
+ ### Scheduling
61
+ | Tool | Description |
62
+ |------|-------------|
63
+ | `linkedin_schedule_post` | Schedule for future publication |
64
+ | `linkedin_list_scheduled_posts` | List by status |
65
+ | `linkedin_cancel_scheduled_post` | Cancel pending post |
66
+ | `linkedin_get_scheduled_post` | Get scheduled post details |
67
+
68
+ ### Authentication
69
+ | Tool | Description |
70
+ |------|-------------|
71
+ | `linkedin_get_auth_url` | Start OAuth flow |
72
+ | `linkedin_exchange_code` | Complete OAuth |
73
+ | `linkedin_refresh_token` | Refresh expired token |
74
+ | `linkedin_get_user_info` | Get profile info |
75
+
76
+ ## Setup
77
+
78
+ ### Option 1: Quick Setup (Recommended)
79
+ 1. Install the extension
80
+ 2. Ask Claude: "Help me set up LinkedIn authentication"
81
+ 3. Follow the guided OAuth flow
82
+
83
+ ### Option 2: Manual Setup
84
+ 1. Create a LinkedIn Developer App at https://www.linkedin.com/developers/apps
85
+ 2. Add products: "Sign In with LinkedIn using OpenID Connect" + "Share on LinkedIn"
86
+ 3. Configure redirect URI: `https://localhost:8888/callback`
87
+ 4. Set environment variables (see Installation above)
93
88
 
94
- 4. Run tests:
89
+ ## Running the Scheduler
90
+
91
+ For scheduled posts to publish automatically:
95
92
  ```bash
96
- npm test # All 24 tests should pass
93
+ npm run scheduler
97
94
  ```
98
95
 
99
- 5. **Setup with Claude Desktop:**
100
-
101
- See [MCP_SETUP.md](./MCP_SETUP.md) for detailed Claude Desktop configuration instructions.
96
+ This runs a daemon that checks every minute for posts due to publish.
102
97
 
103
- ### Environment Variables
104
-
105
- See `.env.example` for required configuration:
106
- - `LINKEDIN_CLIENT_ID` - Your app's client ID
107
- - `LINKEDIN_CLIENT_SECRET` - Your app's client secret
108
- - `LINKEDIN_REDIRECT_URI` - OAuth callback URL
109
- - `LINKEDIN_PERSON_ID` - Your LinkedIn person URN (from userinfo endpoint)
110
- - `LINKEDIN_ACCESS_TOKEN` - OAuth access token (60-day expiry)
111
-
112
- ## Documentation
113
-
114
- - **[MCP_SETUP.md](./MCP_SETUP.md)** - Claude Desktop configuration and usage guide
115
- - **[USER_STORY.md](./USER_STORY.md)** - Complete user stories and feature roadmap
116
- - **[LINKEDIN_API_REFERENCE.md](./LINKEDIN_API_REFERENCE.md)** - Comprehensive API documentation
117
- - **[src/types.js](./src/types.js)** - JSDoc type definitions
118
- - **[src/schemas.js](./src/schemas.js)** - Zod validation schemas
119
-
120
- ## Testing
98
+ ## Development
121
99
 
122
100
  ```bash
123
- npm test # Run all tests
124
- npm run test:watch # Watch mode
101
+ npm install
102
+ npm test # 118 tests
125
103
  npm run test:coverage # Coverage report
104
+ npm start # Start MCP server
105
+ npm run scheduler # Start scheduler daemon
126
106
  ```
127
107
 
128
- All 24 tests passing with 100% code coverage for core tools!
129
-
130
- ## Contributing
108
+ ## Documentation
131
109
 
132
- Lucas Draney (@ldraney)
110
+ - [CLAUDE.md](./CLAUDE.md) - Project structure and API details
111
+ - [MCP_SETUP.md](./MCP_SETUP.md) - Claude Desktop configuration
112
+ - [LINKEDIN_API_REFERENCE.md](./LINKEDIN_API_REFERENCE.md) - API documentation
133
113
 
134
114
  ## License
135
115
 
package/manifest.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "manifest_version": "0.2",
3
3
  "name": "@ldraney/mcp-linkedin",
4
- "version": "0.1.2",
5
- "description": "MCP server for managing LinkedIn posts - create text, image, video, document, poll, and multi-image posts. Schedule posts, add comments and reactions.",
4
+ "version": "0.2.1",
5
+ "description": "MCP server for LinkedIn - create posts, images, videos, documents, polls. One-click OAuth via built-in relay.",
6
6
  "author": {
7
7
  "name": "Lucas Draney"
8
8
  },
@@ -19,43 +19,22 @@
19
19
  "${__dirname}/src/index.js"
20
20
  ],
21
21
  "env": {
22
- "LINKEDIN_CLIENT_ID": "${user_config.linkedin_client_id}",
23
- "LINKEDIN_CLIENT_SECRET": "${user_config.linkedin_client_secret}",
24
- "LINKEDIN_REDIRECT_URI": "${user_config.linkedin_redirect_uri}",
25
22
  "LINKEDIN_PERSON_ID": "${user_config.linkedin_person_id}",
26
23
  "LINKEDIN_ACCESS_TOKEN": "${user_config.linkedin_access_token}"
27
24
  }
28
25
  }
29
26
  },
30
27
  "user_config": {
31
- "linkedin_client_id": {
32
- "type": "string",
33
- "title": "Client ID (optional)",
34
- "description": "LinkedIn OAuth app client ID - leave blank to set up via chat",
35
- "required": false
36
- },
37
- "linkedin_client_secret": {
38
- "type": "string",
39
- "title": "Client Secret (optional)",
40
- "description": "LinkedIn OAuth app client secret - leave blank to set up via chat",
41
- "required": false
42
- },
43
- "linkedin_redirect_uri": {
44
- "type": "string",
45
- "title": "Redirect URI (optional)",
46
- "description": "OAuth callback URL - defaults to https://localhost:8888/callback",
47
- "required": false
48
- },
49
28
  "linkedin_person_id": {
50
29
  "type": "string",
51
30
  "title": "Person ID (optional)",
52
- "description": "Your LinkedIn person ID - obtained automatically during OAuth",
31
+ "description": "Your LinkedIn person ID - obtained via OAuth flow in chat",
53
32
  "required": false
54
33
  },
55
34
  "linkedin_access_token": {
56
35
  "type": "string",
57
36
  "title": "Access Token (optional)",
58
- "description": "OAuth access token - obtained via linkedin_exchange_code tool",
37
+ "description": "OAuth access token - obtained via OAuth flow in chat",
59
38
  "required": false
60
39
  }
61
40
  },
@@ -0,0 +1,12 @@
1
+ FROM node:18-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package*.json ./
6
+ RUN npm install --production
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 3000
11
+
12
+ CMD ["node", "index.js"]
@@ -0,0 +1,17 @@
1
+ app = 'iss-linkedin-oauth'
2
+ primary_region = 'sjc'
3
+
4
+ [build]
5
+
6
+ [http_service]
7
+ internal_port = 3000
8
+ force_https = true
9
+ auto_stop_machines = 'stop'
10
+ auto_start_machines = true
11
+ min_machines_running = 0
12
+ processes = ['app']
13
+
14
+ [[vm]]
15
+ memory = '256mb'
16
+ cpu_kind = 'shared'
17
+ cpus = 1
@@ -0,0 +1,151 @@
1
+ const express = require('express');
2
+ const https = require('https');
3
+
4
+ const app = express();
5
+ const PORT = process.env.PORT || 3000;
6
+
7
+ const CLIENT_ID = process.env.LINKEDIN_CLIENT_ID;
8
+ const CLIENT_SECRET = process.env.LINKEDIN_CLIENT_SECRET;
9
+ const REDIRECT_URI = process.env.REDIRECT_URI || `https://${process.env.FLY_APP_NAME}.fly.dev/auth/callback`;
10
+ const LOCALHOST_CALLBACK = 'http://localhost:8888/callback';
11
+
12
+ // Health check
13
+ app.get('/', (req, res) => {
14
+ res.json({ status: 'ok', service: 'mcp-linkedin-oauth-relay' });
15
+ });
16
+
17
+ // Step 1: Redirect user to LinkedIn OAuth
18
+ app.get('/auth/linkedin', (req, res) => {
19
+ if (!CLIENT_ID) {
20
+ return res.status(500).json({ error: 'LINKEDIN_CLIENT_ID not configured' });
21
+ }
22
+
23
+ const scope = 'openid profile w_member_social';
24
+ const state = Math.random().toString(36).substring(7);
25
+
26
+ const authUrl = new URL('https://www.linkedin.com/oauth/v2/authorization');
27
+ authUrl.searchParams.set('response_type', 'code');
28
+ authUrl.searchParams.set('client_id', CLIENT_ID);
29
+ authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
30
+ authUrl.searchParams.set('scope', scope);
31
+ authUrl.searchParams.set('state', state);
32
+
33
+ res.redirect(authUrl.toString());
34
+ });
35
+
36
+ // Step 2: Handle callback from LinkedIn, exchange code for token
37
+ app.get('/auth/callback', async (req, res) => {
38
+ const { code, error, error_description } = req.query;
39
+
40
+ if (error) {
41
+ return redirectToLocalhost(res, { error, error_description });
42
+ }
43
+
44
+ if (!code) {
45
+ return redirectToLocalhost(res, { error: 'missing_code', error_description: 'No authorization code received' });
46
+ }
47
+
48
+ try {
49
+ // Exchange code for access token
50
+ const tokenData = await exchangeCodeForToken(code);
51
+
52
+ // Get user info to get person ID
53
+ const userInfo = await getUserInfo(tokenData.access_token);
54
+
55
+ // Redirect to localhost with token and person ID
56
+ redirectToLocalhost(res, {
57
+ access_token: tokenData.access_token,
58
+ expires_in: tokenData.expires_in,
59
+ person_id: userInfo.sub,
60
+ name: userInfo.name
61
+ });
62
+ } catch (err) {
63
+ console.error('OAuth error:', err);
64
+ redirectToLocalhost(res, { error: 'token_exchange_failed', error_description: err.message });
65
+ }
66
+ });
67
+
68
+ function redirectToLocalhost(res, params) {
69
+ const url = new URL(LOCALHOST_CALLBACK);
70
+ Object.entries(params).forEach(([key, value]) => {
71
+ if (value) url.searchParams.set(key, value);
72
+ });
73
+ res.redirect(url.toString());
74
+ }
75
+
76
+ function exchangeCodeForToken(code) {
77
+ return new Promise((resolve, reject) => {
78
+ const postData = new URLSearchParams({
79
+ grant_type: 'authorization_code',
80
+ code,
81
+ client_id: CLIENT_ID,
82
+ client_secret: CLIENT_SECRET,
83
+ redirect_uri: REDIRECT_URI
84
+ }).toString();
85
+
86
+ const options = {
87
+ hostname: 'www.linkedin.com',
88
+ path: '/oauth/v2/accessToken',
89
+ method: 'POST',
90
+ headers: {
91
+ 'Content-Type': 'application/x-www-form-urlencoded',
92
+ 'Content-Length': Buffer.byteLength(postData)
93
+ }
94
+ };
95
+
96
+ const req = https.request(options, (res) => {
97
+ let data = '';
98
+ res.on('data', chunk => data += chunk);
99
+ res.on('end', () => {
100
+ try {
101
+ const parsed = JSON.parse(data);
102
+ if (parsed.error) {
103
+ reject(new Error(parsed.error_description || parsed.error));
104
+ } else {
105
+ resolve(parsed);
106
+ }
107
+ } catch (e) {
108
+ reject(new Error('Failed to parse token response'));
109
+ }
110
+ });
111
+ });
112
+
113
+ req.on('error', reject);
114
+ req.write(postData);
115
+ req.end();
116
+ });
117
+ }
118
+
119
+ function getUserInfo(accessToken) {
120
+ return new Promise((resolve, reject) => {
121
+ const options = {
122
+ hostname: 'api.linkedin.com',
123
+ path: '/v2/userinfo',
124
+ method: 'GET',
125
+ headers: {
126
+ 'Authorization': `Bearer ${accessToken}`
127
+ }
128
+ };
129
+
130
+ const req = https.request(options, (res) => {
131
+ let data = '';
132
+ res.on('data', chunk => data += chunk);
133
+ res.on('end', () => {
134
+ try {
135
+ resolve(JSON.parse(data));
136
+ } catch (e) {
137
+ reject(new Error('Failed to parse user info'));
138
+ }
139
+ });
140
+ });
141
+
142
+ req.on('error', reject);
143
+ req.end();
144
+ });
145
+ }
146
+
147
+ app.listen(PORT, () => {
148
+ console.log(`OAuth relay running on port ${PORT}`);
149
+ console.log(`Auth URL: /auth/linkedin`);
150
+ console.log(`Callback: ${REDIRECT_URI}`);
151
+ });