k-linkedin-mcp 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/.dockerignore +15 -0
- package/.gitlab-ci.yml +78 -0
- package/CHANGELOG.md +16 -0
- package/Dockerfile +27 -0
- package/README.md +235 -0
- package/config.json +18 -0
- package/package.json +41 -0
- package/src/.env.example +17 -0
- package/src/admin/cli.js +145 -0
- package/src/admin/oauth.js +170 -0
- package/src/admin/service.js +122 -0
- package/src/admin/telegram.js +150 -0
- package/src/admin.js +13 -0
- package/src/client.js +279 -0
- package/src/config.js +99 -0
- package/src/http_app.js +203 -0
- package/src/main.js +62 -0
- package/src/server.js +42 -0
- package/src/token.js +96 -0
- package/src/tools/guide.js +82 -0
- package/src/tools/posts.js +137 -0
- package/src/tools/profile.js +42 -0
- package/src/tools/registry.js +68 -0
- package/src/tools/social.js +77 -0
- package/tests/admin_service.test.js +43 -0
- package/tests/config.test.js +68 -0
- package/tests/telegram.test.js +65 -0
- package/tests/token.test.js +99 -0
- package/tests/tools.test.js +230 -0
package/.dockerignore
ADDED
package/.gitlab-ci.yml
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# linkedin-mcp — GitLab CI/CD Pipeline
|
|
2
|
+
# Stages: test → build image → deploy to homelab
|
|
3
|
+
|
|
4
|
+
stages:
|
|
5
|
+
- test
|
|
6
|
+
- build
|
|
7
|
+
- deploy
|
|
8
|
+
|
|
9
|
+
variables:
|
|
10
|
+
IMAGE_NAME: registry.gitlab.com/kpihx-labs/linkedin-mcp
|
|
11
|
+
CONTAINER_NAME: linkedin_mcp
|
|
12
|
+
|
|
13
|
+
# ── Test ──────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
test:
|
|
16
|
+
stage: test
|
|
17
|
+
image: oven/bun:1-slim
|
|
18
|
+
script:
|
|
19
|
+
- bun install --frozen-lockfile
|
|
20
|
+
- bun test
|
|
21
|
+
coverage: '/\d+ pass/'
|
|
22
|
+
artifacts:
|
|
23
|
+
reports:
|
|
24
|
+
junit: test-results.xml
|
|
25
|
+
when: always
|
|
26
|
+
expire_in: 7 days
|
|
27
|
+
rules:
|
|
28
|
+
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
|
29
|
+
- if: '$CI_COMMIT_BRANCH == "main"'
|
|
30
|
+
- if: '$CI_COMMIT_BRANCH == "develop"'
|
|
31
|
+
|
|
32
|
+
# ── Build Docker image ────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
build:
|
|
35
|
+
stage: build
|
|
36
|
+
image: docker:27
|
|
37
|
+
services:
|
|
38
|
+
- docker:27-dind
|
|
39
|
+
before_script:
|
|
40
|
+
- echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin
|
|
41
|
+
script:
|
|
42
|
+
- docker build -t $IMAGE_NAME:$CI_COMMIT_SHORT_SHA -t $IMAGE_NAME:latest .
|
|
43
|
+
- docker push $IMAGE_NAME:$CI_COMMIT_SHORT_SHA
|
|
44
|
+
- docker push $IMAGE_NAME:latest
|
|
45
|
+
rules:
|
|
46
|
+
- if: '$CI_COMMIT_BRANCH == "main"'
|
|
47
|
+
|
|
48
|
+
# ── Deploy to Homelab ─────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
deploy:
|
|
51
|
+
stage: deploy
|
|
52
|
+
image: alpine:3.20
|
|
53
|
+
before_script:
|
|
54
|
+
- apk add --no-cache openssh-client
|
|
55
|
+
- mkdir -p ~/.ssh
|
|
56
|
+
- echo "$HOMELAB_SSH_KEY" > ~/.ssh/id_ed25519
|
|
57
|
+
- chmod 600 ~/.ssh/id_ed25519
|
|
58
|
+
- ssh-keyscan -H $HOMELAB_HOST >> ~/.ssh/known_hosts
|
|
59
|
+
script:
|
|
60
|
+
- |
|
|
61
|
+
ssh -i ~/.ssh/id_ed25519 kpihx@$HOMELAB_HOST "
|
|
62
|
+
echo '$CI_REGISTRY_PASSWORD' | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin
|
|
63
|
+
docker pull $IMAGE_NAME:latest
|
|
64
|
+
cd ~/stacks/linkedin-mcp && docker compose pull && docker compose up -d --remove-orphans
|
|
65
|
+
docker image prune -f
|
|
66
|
+
"
|
|
67
|
+
environment:
|
|
68
|
+
name: homelab
|
|
69
|
+
url: https://linkedin.kpihx-labs.com
|
|
70
|
+
rules:
|
|
71
|
+
- if: '$CI_COMMIT_BRANCH == "main"'
|
|
72
|
+
needs: [build]
|
|
73
|
+
|
|
74
|
+
# ── Variables to define in GitLab CI/CD Settings → Variables ─────────────────
|
|
75
|
+
# HOMELAB_SSH_KEY — private SSH key for homelab deploy user (masked, protected)
|
|
76
|
+
# HOMELAB_HOST — homelab IP/hostname reachable from CI runner (masked)
|
|
77
|
+
# CI_REGISTRY_USER — GitLab container registry user (auto)
|
|
78
|
+
# CI_REGISTRY_PASSWORD — GitLab registry password / deploy token (masked)
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog — linkedin-mcp
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## [0.1.0] — 2026-03-22
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- [x] Full MCP server — 8 tools: guide, auth_status, get_profile, create_post, create_image_post, delete_post, like_post, create_comment
|
|
9
|
+
- [x] LinkedIn OAuth 2.0 + OIDC flow (`linkedin-admin auth`)
|
|
10
|
+
- [x] Three-level admin: CLI (`linkedin-admin`) · HTTP (`/admin/*`) · Telegram bot
|
|
11
|
+
- [x] Dual transport: stdio + Streamable HTTP (Express, port 8095)
|
|
12
|
+
- [x] Token lifecycle management — 60-day expiry tracking, `tokenSummary` with days_left
|
|
13
|
+
- [x] Test suite — 41 tests, 5 files, 0 live API calls (mocked fetch + temp dirs)
|
|
14
|
+
- [x] Docker image (`oven/bun:1-slim`) + `deploy/docker-compose.yml` with Traefik labels
|
|
15
|
+
- [x] GitLab CI — test → build → deploy pipeline
|
|
16
|
+
- [x] Editable install via `bun link`
|
package/Dockerfile
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
FROM oven/bun:1-slim
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
COPY package.json bun.lock* ./
|
|
6
|
+
RUN bun install --frozen-lockfile --production
|
|
7
|
+
|
|
8
|
+
COPY src ./src
|
|
9
|
+
COPY config.json ./config.json
|
|
10
|
+
COPY README.md ./README.md
|
|
11
|
+
COPY CHANGELOG.md ./CHANGELOG.md
|
|
12
|
+
|
|
13
|
+
RUN chmod +x /app/src/main.js /app/src/admin.js /app/src/admin/cli.js \
|
|
14
|
+
&& ln -sf /app/src/main.js /usr/local/bin/linkedin-mcp \
|
|
15
|
+
&& ln -sf /app/src/admin.js /usr/local/bin/linkedin-admin
|
|
16
|
+
|
|
17
|
+
ENV NODE_ENV=production
|
|
18
|
+
ENV LINKEDIN_STATE_DIR=/data/state
|
|
19
|
+
ENV LINKEDIN_MCP_HTTP_HOST=0.0.0.0
|
|
20
|
+
ENV LINKEDIN_MCP_HTTP_PORT=8095
|
|
21
|
+
ENV LINKEDIN_MCP_HTTP_PATH=/mcp
|
|
22
|
+
|
|
23
|
+
VOLUME ["/data"]
|
|
24
|
+
|
|
25
|
+
EXPOSE 8095
|
|
26
|
+
|
|
27
|
+
CMD ["bun", "src/main.js", "serve-http"]
|
package/README.md
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# linkedin-mcp
|
|
2
|
+
|
|
3
|
+
> **0 Trust · 100% Control | 0 Magic · 100% Transparency | 0 Hardcoding · 100% Flexibility**
|
|
4
|
+
|
|
5
|
+
A **Model Context Protocol (MCP) server** for LinkedIn — official OAuth 2.0, intent-first tool surface, three-level admin (CLI · HTTP · Telegram). Designed for the KpihX homelab stack; deployment-ready with Docker + Traefik.
|
|
6
|
+
|
|
7
|
+
**Repos:** [GitHub](https://github.com/KpihX/linkedin-mcp) · [GitLab](https://gitlab.com/kpihx-labs/linkedin-mcp)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why this exists
|
|
12
|
+
|
|
13
|
+
LinkedIn has no general-purpose developer API for personal accounts. The official OAuth surface (`openid profile email w_member_social`) covers everything an individual power-user needs:
|
|
14
|
+
|
|
15
|
+
- Post, delete, like, comment on content
|
|
16
|
+
- Read own profile and auth status
|
|
17
|
+
- Guide agents through the flow with `linkedin_guide`
|
|
18
|
+
|
|
19
|
+
This MCP wraps that surface with **full operational visibility**: a CLI admin, an HTTP admin API, and an optional Telegram bot — the same architecture as `whats-mcp`, `tick-mcp`, `mail-mcp`.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Architecture
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
linkedin-mcp/
|
|
27
|
+
├── src/
|
|
28
|
+
│ ├── main.js ← entry: serve (stdio) | serve-http
|
|
29
|
+
│ ├── admin.js ← entry: CLI admin (linkedin-admin)
|
|
30
|
+
│ ├── config.js ← loadConfig() — deep merge defaults → config.json → env
|
|
31
|
+
│ ├── token.js ← save / load / clear / summary (~/.mcps/linkedin/token.json)
|
|
32
|
+
│ ├── client.js ← LinkedInClient — official REST API calls
|
|
33
|
+
│ ├── server.js ← createMcpServer() — MCP SDK wiring
|
|
34
|
+
│ ├── http_app.js ← Express HTTP app: /health /admin/* /mcp
|
|
35
|
+
│ ├── .env.example ← required secrets template
|
|
36
|
+
│ └── admin/
|
|
37
|
+
│ ├── cli.js ← Commander CLI (auth / status / logout / logs / health / urls / guide)
|
|
38
|
+
│ ├── service.js ← shared text helpers (statusSummaryText, healthSummaryText, …)
|
|
39
|
+
│ ├── oauth.js ← LinkedIn OAuth 2.0 + OIDC flow
|
|
40
|
+
│ └── telegram.js ← optional Telegram bot admin
|
|
41
|
+
├── tools/
|
|
42
|
+
│ ├── registry.js ← listTools() + callTool()
|
|
43
|
+
│ ├── guide.js ← linkedin_guide
|
|
44
|
+
│ ├── profile.js ← linkedin_get_profile, linkedin_auth_status
|
|
45
|
+
│ ├── posts.js ← linkedin_create_post, linkedin_create_image_post, linkedin_delete_post
|
|
46
|
+
│ └── social.js ← linkedin_like_post, linkedin_create_comment
|
|
47
|
+
├── tests/ ← bun test (41 tests, 0 deps on live API)
|
|
48
|
+
├── deploy/
|
|
49
|
+
│ ├── docker-compose.yml
|
|
50
|
+
│ └── docker-compose.override.example.yml
|
|
51
|
+
├── Dockerfile
|
|
52
|
+
├── .gitlab-ci.yml
|
|
53
|
+
└── config.json ← non-secret defaults (port 8095, state dir, scopes)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Transport strategy:**
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
Agent / Claude / Gemini
|
|
60
|
+
│
|
|
61
|
+
├─── HTTP (homelab) ──→ https://linkedin.kpihx-labs.com/mcp (primary)
|
|
62
|
+
└─── stdio (local) ──→ ~/.bun/bin/linkedin-mcp serve (fallback)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Tools (8)
|
|
68
|
+
|
|
69
|
+
| Tool | Description |
|
|
70
|
+
|------|-------------|
|
|
71
|
+
| `linkedin_guide` | Orientation — all tools, auth flow, quick start |
|
|
72
|
+
| `linkedin_auth_status` | Token presence, validity, days remaining, profile |
|
|
73
|
+
| `linkedin_get_profile` | Fetch own LinkedIn profile (name, email, sub) |
|
|
74
|
+
| `linkedin_create_post` | Create a text post (PUBLIC or CONNECTIONS) |
|
|
75
|
+
| `linkedin_create_image_post` | Create a post with an attached image |
|
|
76
|
+
| `linkedin_delete_post` | Delete a post by URN |
|
|
77
|
+
| `linkedin_like_post` | Like a post by URN |
|
|
78
|
+
| `linkedin_create_comment` | Add a comment to a post by URN |
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Admin interfaces
|
|
83
|
+
|
|
84
|
+
### CLI (`linkedin-admin`)
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
linkedin-admin auth # Run OAuth flow — opens browser, saves token
|
|
88
|
+
linkedin-admin status # Token status + profile
|
|
89
|
+
linkedin-admin status --json # Machine-readable JSON
|
|
90
|
+
linkedin-admin logout # Clear saved token
|
|
91
|
+
linkedin-admin logs --lines 50
|
|
92
|
+
linkedin-admin health # HTTP server reachability
|
|
93
|
+
linkedin-admin urls # All public/private endpoints
|
|
94
|
+
linkedin-admin guide # Full help text
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### HTTP admin
|
|
98
|
+
|
|
99
|
+
| Endpoint | Description |
|
|
100
|
+
|----------|-------------|
|
|
101
|
+
| `GET /health` | Liveness probe |
|
|
102
|
+
| `GET /admin/status` | Token status + Telegram runtime state |
|
|
103
|
+
| `GET /admin/help` | All commands reference |
|
|
104
|
+
| `GET /admin/logs?lines=50` | Recent admin log tail |
|
|
105
|
+
| `POST /mcp` | MCP Streamable HTTP transport |
|
|
106
|
+
|
|
107
|
+
### Telegram bot (optional)
|
|
108
|
+
|
|
109
|
+
Set `TELEGRAM_LINKEDIN_TOKEN` + `TELEGRAM_CHAT_IDS` and the server polls every 5 s.
|
|
110
|
+
|
|
111
|
+
| Command | Action |
|
|
112
|
+
|---------|--------|
|
|
113
|
+
| `/start` `/help` | Full command reference |
|
|
114
|
+
| `/status` | Token + service status |
|
|
115
|
+
| `/health` | HTTP server health |
|
|
116
|
+
| `/urls` | Endpoint map |
|
|
117
|
+
| `/logs [n]` | Last n admin log lines (default 20) |
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Quick start
|
|
122
|
+
|
|
123
|
+
### 1. LinkedIn App setup
|
|
124
|
+
|
|
125
|
+
1. Go to [LinkedIn Developer Portal](https://developer.linkedin.com)
|
|
126
|
+
2. Create an app → add products: **"Sign In with LinkedIn using OpenID Connect"** + **"Share on LinkedIn"**
|
|
127
|
+
3. Set redirect URI: `http://localhost:3000/callback`
|
|
128
|
+
4. Note `Client ID` and `Client Secret`
|
|
129
|
+
|
|
130
|
+
### 2. Configure secrets
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
cp src/.env.example .env
|
|
134
|
+
# Fill in LINKEDIN_CLIENT_ID and LINKEDIN_CLIENT_SECRET
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 3. Install & authenticate
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
bun install
|
|
141
|
+
bun link # editable install: ~/.bun/bin/linkedin-mcp + linkedin-admin
|
|
142
|
+
|
|
143
|
+
linkedin-admin auth # opens browser → authorizes → saves token
|
|
144
|
+
linkedin-admin status # verify token is valid
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### 4. Start MCP server
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# stdio (for agent config)
|
|
151
|
+
linkedin-mcp serve
|
|
152
|
+
|
|
153
|
+
# HTTP (for homelab deployment)
|
|
154
|
+
linkedin-mcp serve-http
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Docker / Homelab deployment
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
cp deploy/docker-compose.yml docker-compose.yml
|
|
163
|
+
cp deploy/docker-compose.override.example.yml docker-compose.override.yml
|
|
164
|
+
# Edit docker-compose.override.yml with your secrets
|
|
165
|
+
|
|
166
|
+
docker compose up -d
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Traefik labels in `deploy/docker-compose.yml` expose:
|
|
170
|
+
- `https://linkedin.kpihx-labs.com` (primary private trusted route)
|
|
171
|
+
- `https://linkedin.homelab` (fallback)
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Agent registration
|
|
176
|
+
|
|
177
|
+
### Claude Code (`~/.claude.json`)
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
"linkedin-mcp": {
|
|
181
|
+
"type": "http",
|
|
182
|
+
"url": "https://linkedin.kpihx-labs.com/mcp"
|
|
183
|
+
},
|
|
184
|
+
"linkedin-mcp--fallback": {
|
|
185
|
+
"command": "/home/kpihx/.bun/bin/linkedin-mcp",
|
|
186
|
+
"args": ["serve"]
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Gemini (`~/.gemini/extensions/linkedin_mcp/gemini-extension.json`)
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"name": "linkedin-mcp",
|
|
195
|
+
"version": "0.1.0",
|
|
196
|
+
"mcpServers": {
|
|
197
|
+
"linkedin-mcp": {
|
|
198
|
+
"httpUrl": "https://linkedin.kpihx-labs.com/mcp"
|
|
199
|
+
},
|
|
200
|
+
"linkedin-mcp--fallback": {
|
|
201
|
+
"command": "/home/kpihx/.bun/bin/linkedin-mcp",
|
|
202
|
+
"args": ["serve"]
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Tests
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
bun test # 41 tests, 5 files, 0 live API calls
|
|
214
|
+
bun test --watch # watch mode
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
All tests use mocked `fetch` and temp directories — no credentials needed.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Token lifecycle
|
|
222
|
+
|
|
223
|
+
LinkedIn personal OAuth tokens expire in **60 days**. There is no refresh token for non-Partner apps.
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
Day 0 → linkedin-admin auth (OAuth flow, browser consent)
|
|
227
|
+
Day 55+ → linkedin-admin status (shows days_left, warns if < 7)
|
|
228
|
+
Day 60 → token invalid, re-run auth
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT — see [LICENSE](LICENSE).
|
package/config.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"server": {
|
|
3
|
+
"state_directory": "~/.mcps/linkedin",
|
|
4
|
+
"http_host": "127.0.0.1",
|
|
5
|
+
"http_port": 8095,
|
|
6
|
+
"http_mcp_path": "/mcp",
|
|
7
|
+
"public_base_url": "https://linkedin.kpihx-labs.com",
|
|
8
|
+
"fallback_base_url": "https://linkedin.homelab"
|
|
9
|
+
},
|
|
10
|
+
"oauth": {
|
|
11
|
+
"redirect_port": 3000,
|
|
12
|
+
"redirect_uri": "http://localhost:3000/callback",
|
|
13
|
+
"scopes": ["openid", "profile", "email", "w_member_social"]
|
|
14
|
+
},
|
|
15
|
+
"logging": {
|
|
16
|
+
"level": "error"
|
|
17
|
+
}
|
|
18
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "k-linkedin-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for LinkedIn — intent-first posting, profile management, and professional networking over stdio and HTTP.",
|
|
5
|
+
"main": "src/main.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"linkedin-mcp": "src/main.js",
|
|
8
|
+
"linkedin-admin": "src/admin.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "bun src/main.js serve",
|
|
12
|
+
"serve:http": "bun src/main.js serve-http",
|
|
13
|
+
"admin": "bun src/admin.js",
|
|
14
|
+
"auth": "bun src/admin.js auth",
|
|
15
|
+
"status": "bun src/admin.js status",
|
|
16
|
+
"test": "bun test",
|
|
17
|
+
"test:watch": "bun test --watch"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"linkedin",
|
|
21
|
+
"mcp",
|
|
22
|
+
"model-context-protocol",
|
|
23
|
+
"oauth",
|
|
24
|
+
"professional-network",
|
|
25
|
+
"automation"
|
|
26
|
+
],
|
|
27
|
+
"author": "Ivann KAMDEM <kapoivha@gmail.com>",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"type": "commonjs",
|
|
30
|
+
"engines": {
|
|
31
|
+
"bun": ">=1.0.0",
|
|
32
|
+
"node": ">=18.0.0"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
36
|
+
"commander": "^13.1.0",
|
|
37
|
+
"express": "^5.2.1",
|
|
38
|
+
"pino": "^10.3.1"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {}
|
|
41
|
+
}
|
package/src/.env.example
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# LinkedIn OAuth credentials — register your app at https://developer.linkedin.com
|
|
2
|
+
# Products to add: "Sign In with LinkedIn using OpenID Connect" + "Share on LinkedIn"
|
|
3
|
+
LINKEDIN_CLIENT_ID=your_client_id_here
|
|
4
|
+
LINKEDIN_CLIENT_SECRET=your_client_secret_here
|
|
5
|
+
|
|
6
|
+
# Optional: pre-set access token (bypasses oauth flow). Populated by: linkedin-admin auth
|
|
7
|
+
# LINKEDIN_ACCESS_TOKEN=your_access_token_here
|
|
8
|
+
|
|
9
|
+
# Optional: Telegram admin bot
|
|
10
|
+
# TELEGRAM_LINKEDIN_TOKEN=your_bot_token_here (from @BotFather)
|
|
11
|
+
# TELEGRAM_CHAT_IDS=123456789,987654321 (comma-separated chat IDs)
|
|
12
|
+
|
|
13
|
+
# Optional server overrides
|
|
14
|
+
# LINKEDIN_MCP_HTTP_HOST=0.0.0.0
|
|
15
|
+
# LINKEDIN_MCP_HTTP_PORT=8095
|
|
16
|
+
# LINKEDIN_MCP_LOG_LEVEL=info
|
|
17
|
+
# LINKEDIN_STATE_DIR=~/.mcps/linkedin
|
package/src/admin/cli.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* linkedin-mcp — Admin CLI (linkedin-admin).
|
|
4
|
+
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* auth Start OAuth 2.0 flow (opens browser, saves token)
|
|
7
|
+
* status [--json] Show token + member info
|
|
8
|
+
* logout Clear stored token
|
|
9
|
+
* logs [--lines N] Tail the admin log
|
|
10
|
+
* health Show HTTP endpoint URLs
|
|
11
|
+
* urls Show all public URLs
|
|
12
|
+
* guide Show admin capabilities
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
"use strict";
|
|
16
|
+
|
|
17
|
+
const { Command } = require("commander");
|
|
18
|
+
|
|
19
|
+
const { loadConfig } = require("../config");
|
|
20
|
+
const { tokenSummary, clearToken } = require("../token");
|
|
21
|
+
const { runOAuthFlow } = require("./oauth");
|
|
22
|
+
const {
|
|
23
|
+
adminHelpText,
|
|
24
|
+
getLogsText,
|
|
25
|
+
healthSummaryText,
|
|
26
|
+
statusSummaryText,
|
|
27
|
+
urlsSummary,
|
|
28
|
+
appendAdminLog,
|
|
29
|
+
} = require("./service");
|
|
30
|
+
|
|
31
|
+
const PKG = require("../../package.json");
|
|
32
|
+
|
|
33
|
+
const program = new Command();
|
|
34
|
+
program
|
|
35
|
+
.name("linkedin-admin")
|
|
36
|
+
.description("linkedin-mcp administration: auth, status, logout, logs")
|
|
37
|
+
.version(PKG.version);
|
|
38
|
+
|
|
39
|
+
// ── auth ─────────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
program
|
|
42
|
+
.command("auth")
|
|
43
|
+
.description("Start LinkedIn OAuth 2.0 flow. Opens a browser for authorization.")
|
|
44
|
+
.action(async () => {
|
|
45
|
+
const config = loadConfig();
|
|
46
|
+
if (!config.oauth.client_id || !config.oauth.client_secret) {
|
|
47
|
+
console.error(
|
|
48
|
+
"\nError: LINKEDIN_CLIENT_ID and LINKEDIN_CLIENT_SECRET are not set.\n" +
|
|
49
|
+
"Steps:\n" +
|
|
50
|
+
" 1. Create app at https://developer.linkedin.com\n" +
|
|
51
|
+
" 2. Add products: 'Sign In with LinkedIn using OpenID Connect' + 'Share on LinkedIn'\n" +
|
|
52
|
+
" 3. Set redirect URI: http://localhost:3000/callback\n" +
|
|
53
|
+
" 4. Store credentials via bw-env: LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET\n",
|
|
54
|
+
);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
console.log("\nStarting LinkedIn OAuth flow...");
|
|
58
|
+
console.log(`Redirect URI : ${config.oauth.redirect_uri}`);
|
|
59
|
+
console.log(`Scopes : ${config.oauth.scopes.join(", ")}\n`);
|
|
60
|
+
try {
|
|
61
|
+
const tokenData = await runOAuthFlow(config, (url) => {
|
|
62
|
+
console.log("Open this URL in your browser:\n");
|
|
63
|
+
console.log(` ${url}\n`);
|
|
64
|
+
const { execSync } = require("child_process");
|
|
65
|
+
try { execSync(`xdg-open "${url}"`, { stdio: "ignore" }); } catch { /* silent */ }
|
|
66
|
+
});
|
|
67
|
+
appendAdminLog(`auth success member=${tokenData.member_id} name=${tokenData.name}`);
|
|
68
|
+
console.log(`\nAuthenticated: ${tokenData.name || "LinkedIn member"}`);
|
|
69
|
+
console.log(`Email : ${tokenData.email || "N/A"}`);
|
|
70
|
+
console.log(`Expires in : ${Math.floor((tokenData.expires_in || 5184000) / 86400)} days`);
|
|
71
|
+
console.log("\nToken saved. linkedin-mcp is ready.\n");
|
|
72
|
+
} catch (err) {
|
|
73
|
+
appendAdminLog(`auth error ${err.message}`);
|
|
74
|
+
console.error(`\nAuth failed: ${err.message}\n`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ── status ────────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
program
|
|
82
|
+
.command("status")
|
|
83
|
+
.description("Show current authentication status and token details.")
|
|
84
|
+
.option("--json", "Output raw JSON")
|
|
85
|
+
.action((opts) => {
|
|
86
|
+
const config = loadConfig();
|
|
87
|
+
const summary = tokenSummary(config);
|
|
88
|
+
if (opts.json) {
|
|
89
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
90
|
+
} else {
|
|
91
|
+
console.log(`\n${statusSummaryText()}\n`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ── logout ────────────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
program
|
|
98
|
+
.command("logout")
|
|
99
|
+
.description("Clear the stored LinkedIn access token.")
|
|
100
|
+
.action(() => {
|
|
101
|
+
const config = loadConfig();
|
|
102
|
+
clearToken(config);
|
|
103
|
+
appendAdminLog("logout: token cleared");
|
|
104
|
+
console.log("\nToken cleared. Run 'linkedin-admin auth' to re-authenticate.\n");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ── logs ──────────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
program
|
|
110
|
+
.command("logs")
|
|
111
|
+
.description("Show recent admin log lines.")
|
|
112
|
+
.option("--lines <n>", "Number of lines to show", "50")
|
|
113
|
+
.action((opts) => {
|
|
114
|
+
const limit = parseInt(opts.lines, 10) || 50;
|
|
115
|
+
console.log(getLogsText(limit));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ── health ────────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
program
|
|
121
|
+
.command("health")
|
|
122
|
+
.description("Show HTTP endpoint URLs and health check info.")
|
|
123
|
+
.action(() => {
|
|
124
|
+
console.log(`\n${healthSummaryText()}\n`);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ── urls ──────────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
program
|
|
130
|
+
.command("urls")
|
|
131
|
+
.description("Show all public and fallback endpoint URLs.")
|
|
132
|
+
.action(() => {
|
|
133
|
+
console.log(`\n${urlsSummary()}\n`);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ── guide ─────────────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
program
|
|
139
|
+
.command("guide")
|
|
140
|
+
.description("Show full admin capabilities (CLI + HTTP + Telegram).")
|
|
141
|
+
.action(() => {
|
|
142
|
+
console.log(`\n${adminHelpText()}\n`);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
module.exports = { program };
|