@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 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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
@@ -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":";AACA,OAAO,eAAe,CAAC;AACvB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,SAAS,EAA4B,MAAM,WAAW,CAAC;AAE7G,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;;;;;GAKG;AACH,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,uBAAuB,CAAC;IAC7E,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC9C,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAChD,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,WAAW,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAExF,MAAM,OAAO,GAAG,qBAAqB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAErD,MAAM,IAAI,GAAG,MAAM,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACzD,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YAC5C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,UAAU,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;YAClE,IAAI,GAAG,CAAC,QAAQ,KAAK,WAAW,CAAC,QAAQ,EAAE,CAAC;gBAC1C,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;gBACzB,OAAO;YACT,CAAC;YAED,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YAC5C,MAAM,aAAa,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACpD,MAAM,QAAQ,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YAE9C,IAAI,KAAK,EAAE,CAAC;gBACV,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,CAAC,GAAG,CAAC,kCAAkC,KAAK,EAAE,CAAC,CAAC;gBACpG,MAAM,CAAC,KAAK,EAAE,CAAC;gBACf,MAAM,CAAC,IAAI,KAAK,CAAC,kCAAkC,KAAK,EAAE,CAAC,CAAC,CAAC;gBAC7D,OAAO;YACT,CAAC;YACD,IAAI,aAAa,KAAK,KAAK,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACzC,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,CAAC,GAAG,CAAC,6CAA6C,CAAC,CAAC;gBACxG,MAAM,CAAC,KAAK,EAAE,CAAC;gBACf,MAAM,CAAC,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC,CAAC;gBACjE,OAAO;YACT,CAAC;YAED,GAAG;iBACA,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC;iBAChD,GAAG,CAAC,gEAAgE,CAAC,CAAC;YACzE,MAAM,CAAC,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,QAAQ,CAAC,CAAC;QACpB,CAAC,CAAC,CAAC;QAEH,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;YACvB,OAAO,CAAC,KAAK,CAAC,4DAA4D,CAAC,CAAC;YAC5E,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YACvB,OAAO,CAAC,KAAK,CAAC,iCAAiC,MAAM,CAAC,WAAW,MAAM,CAAC,CAAC;QAC3E,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,MAAM,KAAK,GAAG,MAAM,oBAAoB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACvD,MAAM,SAAS,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;IAElC,OAAO,CAAC,KAAK,CAAC,gCAAgC,SAAS,GAAG,CAAC,CAAC;IAC5D,IAAI,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC;QACxB,OAAO,CAAC,KAAK,CACX,oGAAoG;YAClG,yEAAyE,CAC5E,CAAC;IACJ,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,OAAO,CAAC,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACtE,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
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
@@ -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"}
@@ -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,4 @@
1
+ export { TokenManager, buildAuthorizationUrl, exchangeCodeForToken, refreshAccessToken, loadToken, saveToken } from "./auth.js";
2
+ export { LinkedInClient } from "./linkedin.js";
3
+ export { createServer } from "./server.js";
4
+ //# sourceMappingURL=index.js.map
@@ -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
+ }
@@ -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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"}
@@ -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
+ }