@clickonsearch/linkedin-mcp-server 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/README.md +92 -0
- package/dist/auth.d.ts +32 -0
- package/dist/auth.js +94 -0
- package/dist/auth.js.map +1 -0
- package/dist/authorize-cli.d.ts +2 -0
- package/dist/authorize-cli.js +80 -0
- package/dist/authorize-cli.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +47 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/linkedin.d.ts +21 -0
- package/dist/linkedin.js +78 -0
- package/dist/linkedin.js.map +1 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.js +55 -0
- package/dist/server.js.map +1 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# @clickonsearch/linkedin-mcp-server
|
|
2
|
+
|
|
3
|
+
MCP server for a **personal** LinkedIn account via LinkedIn's OAuth 2.0 API.
|
|
4
|
+
Exposes exactly two tools: `get_profile` and `create_post`.
|
|
5
|
+
|
|
6
|
+
## Why only two tools
|
|
7
|
+
|
|
8
|
+
LinkedIn's public API is far more restricted than GitHub's or Gmail's. A
|
|
9
|
+
standard, self-serve developer app can only:
|
|
10
|
+
|
|
11
|
+
- Read the authenticated member's own basic profile (OpenID Connect).
|
|
12
|
+
- Publish a post to their own feed (`w_member_social`).
|
|
13
|
+
|
|
14
|
+
**Sending LinkedIn messages is not available via any public API** — it only
|
|
15
|
+
exists under partner programs (Talent Solutions, Sales Navigator) that
|
|
16
|
+
require a formal application and LinkedIn's manual approval; there is no
|
|
17
|
+
self-serve path to it at all.
|
|
18
|
+
|
|
19
|
+
**Replying/commenting on posts** has a documented endpoint
|
|
20
|
+
(`/v2/socialActions/{urn}/comments`), but creating comments requires a
|
|
21
|
+
separate LinkedIn Developer Program product ("Community Management API" or
|
|
22
|
+
similar) that also needs LinkedIn's case-by-case approval — it is not
|
|
23
|
+
included in standard "Sign In with LinkedIn" / "Share on LinkedIn" access.
|
|
24
|
+
This server doesn't implement it; if you get that access approved, adding
|
|
25
|
+
a `create_comment` tool following the same pattern as `create_post` would
|
|
26
|
+
be straightforward.
|
|
27
|
+
|
|
28
|
+
## Setup
|
|
29
|
+
|
|
30
|
+
1. Create an app at [linkedin.com/developers/apps](https://www.linkedin.com/developers/apps).
|
|
31
|
+
2. Under **Products**, add **"Sign In with LinkedIn using OpenID Connect"**
|
|
32
|
+
and **"Share on LinkedIn"** — both are self-serve, no approval wait.
|
|
33
|
+
3. Under **Auth**, add an **Authorized redirect URL** matching
|
|
34
|
+
`LINKEDIN_REDIRECT_URI` below exactly (default `http://localhost:3300/callback`).
|
|
35
|
+
4. Copy the app's **Client ID** and **Client Secret**.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
cd mcp-servers/linkedin-mcp-server
|
|
39
|
+
npm install
|
|
40
|
+
cp .env.example .env
|
|
41
|
+
# fill in LINKEDIN_CLIENT_ID / LINKEDIN_CLIENT_SECRET
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Authorize (one-time)
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm run dev:authorize
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This prints a URL — open it, sign in, approve — and it saves a token to
|
|
51
|
+
`LINKEDIN_TOKEN_PATH` (default `./linkedin-token.json`, gitignored).
|
|
52
|
+
|
|
53
|
+
**Important:** LinkedIn only issues a refresh token to apps it has approved
|
|
54
|
+
for offline access. For a standard app, the access token this saves is
|
|
55
|
+
valid for **~60 days** and there is no automatic renewal — re-run this
|
|
56
|
+
command once it expires (tools will fail with a clear "access token has
|
|
57
|
+
expired" error when that happens).
|
|
58
|
+
|
|
59
|
+
## Run
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm run dev
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- `POST http://localhost:4300/mcp` — the MCP endpoint (Streamable HTTP, stateless).
|
|
66
|
+
- `GET http://localhost:4300/health` — liveness check.
|
|
67
|
+
|
|
68
|
+
## Tools
|
|
69
|
+
|
|
70
|
+
| Tool | Args | Notes |
|
|
71
|
+
| -------------- | ------ | ----------------------------------------------------- |
|
|
72
|
+
| `get_profile` | — | Name, email, picture — from the OpenID Connect userinfo endpoint |
|
|
73
|
+
| `create_post` | `text` | Publishes to the authenticated member's own feed, `visibility: PUBLIC` |
|
|
74
|
+
|
|
75
|
+
## Using it as a library
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { TokenManager, LinkedInClient, createServer } from "./src/index.js";
|
|
79
|
+
|
|
80
|
+
const tokens = new TokenManager(
|
|
81
|
+
{
|
|
82
|
+
clientId: process.env.LINKEDIN_CLIENT_ID!,
|
|
83
|
+
clientSecret: process.env.LINKEDIN_CLIENT_SECRET!,
|
|
84
|
+
redirectUri: "http://localhost:3300/callback",
|
|
85
|
+
scopes: ["openid", "profile", "email", "w_member_social"],
|
|
86
|
+
},
|
|
87
|
+
"./linkedin-token.json",
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const app = createServer(new LinkedInClient(tokens));
|
|
91
|
+
app.listen(4300);
|
|
92
|
+
```
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface LinkedInOAuthConfig {
|
|
2
|
+
clientId: string;
|
|
3
|
+
clientSecret: string;
|
|
4
|
+
redirectUri: string;
|
|
5
|
+
scopes: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface StoredToken {
|
|
8
|
+
accessToken: string;
|
|
9
|
+
refreshToken?: string;
|
|
10
|
+
/** Epoch ms. */
|
|
11
|
+
expiresAt: number;
|
|
12
|
+
scope: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function buildAuthorizationUrl(config: LinkedInOAuthConfig, state: string): string;
|
|
15
|
+
export declare function exchangeCodeForToken(config: LinkedInOAuthConfig, code: string): Promise<StoredToken>;
|
|
16
|
+
export declare function refreshAccessToken(config: LinkedInOAuthConfig, refreshToken: string): Promise<StoredToken>;
|
|
17
|
+
export declare function loadToken(path: string): Promise<StoredToken | undefined>;
|
|
18
|
+
export declare function saveToken(path: string, token: StoredToken): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Loads the persisted token on first use and hands out a valid access
|
|
21
|
+
* token, transparently refreshing it when it's expiring soon — as long as
|
|
22
|
+
* LinkedIn actually issued a refresh token, which it only does for apps
|
|
23
|
+
* approved for a product that grants offline access. Otherwise the token
|
|
24
|
+
* is simply valid for ~60 days and re-authorizing is a manual step.
|
|
25
|
+
*/
|
|
26
|
+
export declare class TokenManager {
|
|
27
|
+
private readonly config;
|
|
28
|
+
private readonly tokenPath;
|
|
29
|
+
private token?;
|
|
30
|
+
constructor(config: LinkedInOAuthConfig, tokenPath: string);
|
|
31
|
+
getValidAccessToken(): Promise<string>;
|
|
32
|
+
}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
const AUTHORIZATION_URL = "https://www.linkedin.com/oauth/v2/authorization";
|
|
3
|
+
const TOKEN_URL = "https://www.linkedin.com/oauth/v2/accessToken";
|
|
4
|
+
export function buildAuthorizationUrl(config, state) {
|
|
5
|
+
const url = new URL(AUTHORIZATION_URL);
|
|
6
|
+
url.searchParams.set("response_type", "code");
|
|
7
|
+
url.searchParams.set("client_id", config.clientId);
|
|
8
|
+
url.searchParams.set("redirect_uri", config.redirectUri);
|
|
9
|
+
url.searchParams.set("state", state);
|
|
10
|
+
url.searchParams.set("scope", config.scopes.join(" "));
|
|
11
|
+
return url.toString();
|
|
12
|
+
}
|
|
13
|
+
async function requestToken(body) {
|
|
14
|
+
const res = await fetch(TOKEN_URL, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
17
|
+
body,
|
|
18
|
+
});
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
throw new Error(`LinkedIn token request failed: ${res.status} ${await res.text()}`);
|
|
21
|
+
}
|
|
22
|
+
const data = (await res.json());
|
|
23
|
+
return {
|
|
24
|
+
accessToken: data.access_token,
|
|
25
|
+
refreshToken: data.refresh_token,
|
|
26
|
+
expiresAt: Date.now() + data.expires_in * 1000,
|
|
27
|
+
scope: data.scope,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function exchangeCodeForToken(config, code) {
|
|
31
|
+
return requestToken(new URLSearchParams({
|
|
32
|
+
grant_type: "authorization_code",
|
|
33
|
+
code,
|
|
34
|
+
redirect_uri: config.redirectUri,
|
|
35
|
+
client_id: config.clientId,
|
|
36
|
+
client_secret: config.clientSecret,
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
export async function refreshAccessToken(config, refreshToken) {
|
|
40
|
+
const token = await requestToken(new URLSearchParams({
|
|
41
|
+
grant_type: "refresh_token",
|
|
42
|
+
refresh_token: refreshToken,
|
|
43
|
+
client_id: config.clientId,
|
|
44
|
+
client_secret: config.clientSecret,
|
|
45
|
+
}));
|
|
46
|
+
// LinkedIn doesn't always re-issue a refresh_token on refresh; keep the old one if so.
|
|
47
|
+
return { ...token, refreshToken: token.refreshToken ?? refreshToken };
|
|
48
|
+
}
|
|
49
|
+
export async function loadToken(path) {
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(await readFile(path, "utf-8"));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function saveToken(path, token) {
|
|
58
|
+
await writeFile(path, JSON.stringify(token, null, 2), "utf-8");
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Loads the persisted token on first use and hands out a valid access
|
|
62
|
+
* token, transparently refreshing it when it's expiring soon — as long as
|
|
63
|
+
* LinkedIn actually issued a refresh token, which it only does for apps
|
|
64
|
+
* approved for a product that grants offline access. Otherwise the token
|
|
65
|
+
* is simply valid for ~60 days and re-authorizing is a manual step.
|
|
66
|
+
*/
|
|
67
|
+
export class TokenManager {
|
|
68
|
+
config;
|
|
69
|
+
tokenPath;
|
|
70
|
+
token;
|
|
71
|
+
constructor(config, tokenPath) {
|
|
72
|
+
this.config = config;
|
|
73
|
+
this.tokenPath = tokenPath;
|
|
74
|
+
}
|
|
75
|
+
async getValidAccessToken() {
|
|
76
|
+
if (!this.token) {
|
|
77
|
+
this.token = await loadToken(this.tokenPath);
|
|
78
|
+
}
|
|
79
|
+
if (!this.token) {
|
|
80
|
+
throw new Error('Not authorized yet. Run "npm run authorize" (see README) to link your LinkedIn account first.');
|
|
81
|
+
}
|
|
82
|
+
const expiringSoon = Date.now() > this.token.expiresAt - 60_000;
|
|
83
|
+
if (expiringSoon) {
|
|
84
|
+
if (!this.token.refreshToken) {
|
|
85
|
+
throw new Error("LinkedIn access token has expired. LinkedIn only issues refresh tokens to apps approved for " +
|
|
86
|
+
'offline access; without one, re-run "npm run authorize" to get a new token.');
|
|
87
|
+
}
|
|
88
|
+
this.token = await refreshAccessToken(this.config, this.token.refreshToken);
|
|
89
|
+
await saveToken(this.tokenPath, this.token);
|
|
90
|
+
}
|
|
91
|
+
return this.token.accessToken;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
import http from "node:http";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { buildAuthorizationUrl, exchangeCodeForToken, saveToken } from "./auth.js";
|
|
6
|
+
function requireEnv(name) {
|
|
7
|
+
const value = process.env[name];
|
|
8
|
+
if (!value) {
|
|
9
|
+
throw new Error(`Missing ${name} environment variable`);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function loadConfig() {
|
|
14
|
+
return {
|
|
15
|
+
clientId: requireEnv("LINKEDIN_CLIENT_ID"),
|
|
16
|
+
clientSecret: requireEnv("LINKEDIN_CLIENT_SECRET"),
|
|
17
|
+
redirectUri: process.env.LINKEDIN_REDIRECT_URI ?? "http://localhost:3300/callback",
|
|
18
|
+
scopes: (process.env.LINKEDIN_SCOPES ?? "openid profile email w_member_social").split(" "),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* One-time interactive OAuth2 flow: prints a consent URL, runs a throwaway
|
|
23
|
+
* local HTTP server on the redirect URI to catch the authorization code,
|
|
24
|
+
* exchanges it for a token, and saves it to disk. Re-run this whenever the
|
|
25
|
+
* saved token expires and no refresh token was issued (see auth.ts).
|
|
26
|
+
*/
|
|
27
|
+
async function main() {
|
|
28
|
+
const config = loadConfig();
|
|
29
|
+
const tokenPath = process.env.LINKEDIN_TOKEN_PATH ?? "./linkedin-token.json";
|
|
30
|
+
const state = randomBytes(16).toString("hex");
|
|
31
|
+
const redirectUrl = new URL(config.redirectUri);
|
|
32
|
+
const port = Number(redirectUrl.port || (redirectUrl.protocol === "https:" ? 443 : 80));
|
|
33
|
+
const authUrl = buildAuthorizationUrl(config, state);
|
|
34
|
+
const code = await new Promise((resolve, reject) => {
|
|
35
|
+
const server = http.createServer((req, res) => {
|
|
36
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
37
|
+
if (url.pathname !== redirectUrl.pathname) {
|
|
38
|
+
res.writeHead(404).end();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const error = url.searchParams.get("error");
|
|
42
|
+
const returnedState = url.searchParams.get("state");
|
|
43
|
+
const authCode = url.searchParams.get("code");
|
|
44
|
+
if (error) {
|
|
45
|
+
res.writeHead(400, { "Content-Type": "text/plain" }).end(`LinkedIn authorization failed: ${error}`);
|
|
46
|
+
server.close();
|
|
47
|
+
reject(new Error(`LinkedIn authorization failed: ${error}`));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (returnedState !== state || !authCode) {
|
|
51
|
+
res.writeHead(400, { "Content-Type": "text/plain" }).end("Invalid state or missing authorization code");
|
|
52
|
+
server.close();
|
|
53
|
+
reject(new Error("Invalid state or missing authorization code"));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
res
|
|
57
|
+
.writeHead(200, { "Content-Type": "text/plain" })
|
|
58
|
+
.end("Authorized. You can close this tab and return to the terminal.");
|
|
59
|
+
server.close();
|
|
60
|
+
resolve(authCode);
|
|
61
|
+
});
|
|
62
|
+
server.listen(port, () => {
|
|
63
|
+
console.error("Open this URL in a browser to authorize LinkedIn access:\n");
|
|
64
|
+
console.error(authUrl);
|
|
65
|
+
console.error(`\nWaiting for the redirect to ${config.redirectUri} ...`);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
const token = await exchangeCodeForToken(config, code);
|
|
69
|
+
await saveToken(tokenPath, token);
|
|
70
|
+
console.error(`\nAuthorized. Token saved to ${tokenPath}.`);
|
|
71
|
+
if (!token.refreshToken) {
|
|
72
|
+
console.error("Note: LinkedIn did not return a refresh token (only issued to apps approved for offline access).\n" +
|
|
73
|
+
"This access token lasts ~60 days; re-run this command after it expires.");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
main().catch((error) => {
|
|
77
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
});
|
|
80
|
+
//# sourceMappingURL=authorize-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authorize-cli.js","sourceRoot":"","sources":["../src/authorize-cli.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
import { TokenManager } from "./auth.js";
|
|
4
|
+
import { LinkedInClient } from "./linkedin.js";
|
|
5
|
+
import { createServer } from "./server.js";
|
|
6
|
+
function requireEnv(name) {
|
|
7
|
+
const value = process.env[name];
|
|
8
|
+
if (!value) {
|
|
9
|
+
throw new Error(`Missing ${name} environment variable`);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function loadConfig() {
|
|
14
|
+
return {
|
|
15
|
+
clientId: requireEnv("LINKEDIN_CLIENT_ID"),
|
|
16
|
+
clientSecret: requireEnv("LINKEDIN_CLIENT_SECRET"),
|
|
17
|
+
redirectUri: process.env.LINKEDIN_REDIRECT_URI ?? "http://localhost:3300/callback",
|
|
18
|
+
scopes: (process.env.LINKEDIN_SCOPES ?? "openid profile email w_member_social").split(" "),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
async function main() {
|
|
22
|
+
let config;
|
|
23
|
+
try {
|
|
24
|
+
config = loadConfig();
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const tokenPath = process.env.LINKEDIN_TOKEN_PATH ?? "./linkedin-token.json";
|
|
32
|
+
const port = Number(process.env.PORT ?? 4300);
|
|
33
|
+
const tokens = new TokenManager(config, tokenPath);
|
|
34
|
+
const linkedin = new LinkedInClient(tokens);
|
|
35
|
+
const app = createServer(linkedin);
|
|
36
|
+
app.listen(port, () => {
|
|
37
|
+
console.error(`linkedin-mcp-server listening on http://localhost:${port}`);
|
|
38
|
+
console.error(` MCP endpoint: POST http://localhost:${port}/mcp`);
|
|
39
|
+
console.error(` Health: GET http://localhost:${port}/health`);
|
|
40
|
+
console.error(` If tools fail with "Not authorized yet", run: npm run authorize`);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
main().catch((error) => {
|
|
44
|
+
console.error(error);
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
});
|
|
47
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAA4B,MAAM,WAAW,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,SAAS,UAAU,CAAC,IAAY;IAC9B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,WAAW,IAAI,uBAAuB,CAAC,CAAC;IAC1D,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,UAAU;IACjB,OAAO;QACL,QAAQ,EAAE,UAAU,CAAC,oBAAoB,CAAC;QAC1C,YAAY,EAAE,UAAU,CAAC,wBAAwB,CAAC;QAClD,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,gCAAgC;QAClF,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,sCAAsC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC;KAC3F,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,IAAI,MAA2B,CAAC;IAChC,IAAI,CAAC;QACH,MAAM,GAAG,UAAU,EAAE,CAAC;IACxB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACtE,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QACrB,OAAO;IACT,CAAC;IAED,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,uBAAuB,CAAC;IAC7E,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC;IAE9C,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,IAAI,cAAc,CAAC,MAAM,CAAC,CAAC;IAC5C,MAAM,GAAG,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;IAEnC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;QACpB,OAAO,CAAC,KAAK,CAAC,qDAAqD,IAAI,EAAE,CAAC,CAAC;QAC3E,OAAO,CAAC,KAAK,CAAC,yCAAyC,IAAI,MAAM,CAAC,CAAC;QACnE,OAAO,CAAC,KAAK,CAAC,yCAAyC,IAAI,SAAS,CAAC,CAAC;QACtE,OAAO,CAAC,KAAK,CAAC,mEAAmE,CAAC,CAAC;IACrF,CAAC,CAAC,CAAC;AACL,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACrB,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { TokenManager, buildAuthorizationUrl, exchangeCodeForToken, refreshAccessToken, loadToken, saveToken } from "./auth.js";
|
|
2
|
+
export type { LinkedInOAuthConfig, StoredToken } from "./auth.js";
|
|
3
|
+
export { LinkedInClient } from "./linkedin.js";
|
|
4
|
+
export type { LinkedInProfile } from "./linkedin.js";
|
|
5
|
+
export { createServer } from "./server.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEhI,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { TokenManager } from "./auth.js";
|
|
2
|
+
export interface LinkedInProfile {
|
|
3
|
+
sub: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
given_name?: string;
|
|
6
|
+
family_name?: string;
|
|
7
|
+
email?: string;
|
|
8
|
+
picture?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Talks to LinkedIn's OpenID Connect userinfo endpoint and its versioned Posts API. */
|
|
11
|
+
export declare class LinkedInClient {
|
|
12
|
+
private readonly tokens;
|
|
13
|
+
constructor(tokens: TokenManager);
|
|
14
|
+
private authHeaders;
|
|
15
|
+
/** The authenticated member's basic profile, via OpenID Connect (scopes: openid profile email). */
|
|
16
|
+
getProfile(): Promise<LinkedInProfile>;
|
|
17
|
+
/** Publishes a text post to the authenticated member's own feed (scope: w_member_social). */
|
|
18
|
+
createPost(text: string): Promise<{
|
|
19
|
+
id: string;
|
|
20
|
+
}>;
|
|
21
|
+
}
|
package/dist/linkedin.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const API_BASE = "https://api.linkedin.com";
|
|
2
|
+
/**
|
|
3
|
+
* LinkedIn requires a version header (YYYYMM) on its versioned REST APIs
|
|
4
|
+
* (like /rest/posts). LinkedIn ships new versions monthly and only keeps
|
|
5
|
+
* each one supported for roughly a year before sunsetting it — this WILL
|
|
6
|
+
* go stale again. If create_post starts failing with a 426
|
|
7
|
+
* ("NONEXISTENT_VERSION"), check LinkedIn's current API docs
|
|
8
|
+
* (https://learn.microsoft.com/en-us/linkedin/shared/api-guide/concepts/protocol-version)
|
|
9
|
+
* for a current YYYYMM value and set LINKEDIN_API_VERSION.
|
|
10
|
+
*/
|
|
11
|
+
const LINKEDIN_API_VERSION = process.env.LINKEDIN_API_VERSION ?? "202609";
|
|
12
|
+
async function readErrorBody(res) {
|
|
13
|
+
try {
|
|
14
|
+
return await res.text();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Throws a clear, actionable error for a failed LinkedIn API response. */
|
|
21
|
+
async function assertOk(res, action) {
|
|
22
|
+
if (res.ok)
|
|
23
|
+
return;
|
|
24
|
+
const body = await readErrorBody(res);
|
|
25
|
+
if (res.status === 426 || body.includes("NONEXISTENT_VERSION")) {
|
|
26
|
+
throw new Error(`LinkedIn rejected ${action}: the LINKEDIN_API_VERSION this server is sending (${LINKEDIN_API_VERSION}) ` +
|
|
27
|
+
"has been sunset by LinkedIn (it only supports each monthly version for about a year). Set " +
|
|
28
|
+
"LINKEDIN_API_VERSION in your .env to a current YYYYMM value — see " +
|
|
29
|
+
"https://learn.microsoft.com/en-us/linkedin/shared/api-guide/concepts/protocol-version — and restart the server.");
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`LinkedIn ${action} failed: ${res.status} ${body}`);
|
|
32
|
+
}
|
|
33
|
+
/** Talks to LinkedIn's OpenID Connect userinfo endpoint and its versioned Posts API. */
|
|
34
|
+
export class LinkedInClient {
|
|
35
|
+
tokens;
|
|
36
|
+
constructor(tokens) {
|
|
37
|
+
this.tokens = tokens;
|
|
38
|
+
}
|
|
39
|
+
async authHeaders() {
|
|
40
|
+
const accessToken = await this.tokens.getValidAccessToken();
|
|
41
|
+
return { Authorization: `Bearer ${accessToken}` };
|
|
42
|
+
}
|
|
43
|
+
/** The authenticated member's basic profile, via OpenID Connect (scopes: openid profile email). */
|
|
44
|
+
async getProfile() {
|
|
45
|
+
const res = await fetch(`${API_BASE}/v2/userinfo`, { headers: await this.authHeaders() });
|
|
46
|
+
await assertOk(res, "get_profile");
|
|
47
|
+
return (await res.json());
|
|
48
|
+
}
|
|
49
|
+
/** Publishes a text post to the authenticated member's own feed (scope: w_member_social). */
|
|
50
|
+
async createPost(text) {
|
|
51
|
+
const [profile, authHeaders] = await Promise.all([this.getProfile(), this.authHeaders()]);
|
|
52
|
+
const res = await fetch(`${API_BASE}/rest/posts`, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: {
|
|
55
|
+
...authHeaders,
|
|
56
|
+
"Content-Type": "application/json",
|
|
57
|
+
"X-Restli-Protocol-Version": "2.0.0",
|
|
58
|
+
"LinkedIn-Version": LINKEDIN_API_VERSION,
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
author: `urn:li:person:${profile.sub}`,
|
|
62
|
+
commentary: text,
|
|
63
|
+
visibility: "PUBLIC",
|
|
64
|
+
distribution: {
|
|
65
|
+
feedDistribution: "MAIN_FEED",
|
|
66
|
+
targetEntities: [],
|
|
67
|
+
thirdPartyDistributionChannels: [],
|
|
68
|
+
},
|
|
69
|
+
lifecycleState: "PUBLISHED",
|
|
70
|
+
isReshareDisabledByAuthor: false,
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
await assertOk(res, "create_post");
|
|
74
|
+
const id = res.headers.get("x-restli-id") ?? res.headers.get("x-linkedin-id") ?? "unknown";
|
|
75
|
+
return { id };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=linkedin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linkedin.js","sourceRoot":"","sources":["../src/linkedin.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Express } from "express";
|
|
2
|
+
import type { LinkedInClient } from "./linkedin.js";
|
|
3
|
+
/**
|
|
4
|
+
* Builds the Express app: `POST /mcp` is the stateless MCP endpoint (a
|
|
5
|
+
* fresh McpServer per request, matching the SDK's own reference server),
|
|
6
|
+
* `GET /health` is a liveness check. Unlike whatsapp-mcp-server, there's no
|
|
7
|
+
* push/events stream here — LinkedIn's API is pure request/response, there's
|
|
8
|
+
* nothing to watch for.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createServer(linkedin: LinkedInClient): Express;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
|
+
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
5
|
+
function buildMcpServer(linkedin) {
|
|
6
|
+
const server = new McpServer({ name: "linkedin-mcp-server", version: "0.1.0" }, { capabilities: {} });
|
|
7
|
+
server.registerTool("get_profile", { description: "Get the authenticated LinkedIn member's basic profile (name, email, picture)", inputSchema: {} }, async () => {
|
|
8
|
+
const profile = await linkedin.getProfile();
|
|
9
|
+
return { content: [{ type: "text", text: JSON.stringify(profile, null, 2) }] };
|
|
10
|
+
});
|
|
11
|
+
server.registerTool("create_post", {
|
|
12
|
+
description: "Publish a text post to the authenticated member's own LinkedIn feed",
|
|
13
|
+
inputSchema: { text: z.string().describe("Post text") },
|
|
14
|
+
}, async ({ text }) => {
|
|
15
|
+
const result = await linkedin.createPost(text);
|
|
16
|
+
return { content: [{ type: "text", text: `Post published (id: ${result.id})` }] };
|
|
17
|
+
});
|
|
18
|
+
return server;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Builds the Express app: `POST /mcp` is the stateless MCP endpoint (a
|
|
22
|
+
* fresh McpServer per request, matching the SDK's own reference server),
|
|
23
|
+
* `GET /health` is a liveness check. Unlike whatsapp-mcp-server, there's no
|
|
24
|
+
* push/events stream here — LinkedIn's API is pure request/response, there's
|
|
25
|
+
* nothing to watch for.
|
|
26
|
+
*/
|
|
27
|
+
export function createServer(linkedin) {
|
|
28
|
+
const app = createMcpExpressApp();
|
|
29
|
+
app.post("/mcp", async (req, res) => {
|
|
30
|
+
try {
|
|
31
|
+
const server = buildMcpServer(linkedin);
|
|
32
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
33
|
+
await server.connect(transport);
|
|
34
|
+
await transport.handleRequest(req, res, req.body);
|
|
35
|
+
res.on("close", () => {
|
|
36
|
+
void transport.close();
|
|
37
|
+
void server.close();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (!res.headersSent) {
|
|
42
|
+
res.status(500).json({
|
|
43
|
+
jsonrpc: "2.0",
|
|
44
|
+
error: { code: -32603, message: error instanceof Error ? error.message : String(error) },
|
|
45
|
+
id: null,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
app.get("/health", (_req, res) => {
|
|
51
|
+
res.json({ ok: true });
|
|
52
|
+
});
|
|
53
|
+
return app;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clickonsearch/linkedin-mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for a personal LinkedIn account via LinkedIn's OAuth 2.0 API: read your own profile and publish posts to your feed.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/ClickOnSearch/ai-apps.git",
|
|
10
|
+
"directory": "mcp-servers/linkedin-mcp-server"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"main": "dist/index.js",
|
|
19
|
+
"types": "dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"linkedin-mcp-server": "dist/cli.js",
|
|
28
|
+
"linkedin-authorize": "dist/authorize-cli.js"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc -p tsconfig.json",
|
|
32
|
+
"start": "node dist/cli.js",
|
|
33
|
+
"dev": "tsx src/cli.ts",
|
|
34
|
+
"authorize": "node dist/authorize-cli.js",
|
|
35
|
+
"dev:authorize": "tsx src/authorize-cli.ts"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
39
|
+
"express": "^4.21.1",
|
|
40
|
+
"zod": "^3.25.0",
|
|
41
|
+
"dotenv": "^16.4.5"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"typescript": "^5.6.3",
|
|
45
|
+
"tsx": "^4.19.1",
|
|
46
|
+
"@types/node": "^22.7.5",
|
|
47
|
+
"@types/express": "^4.17.21"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18"
|
|
51
|
+
}
|
|
52
|
+
}
|