@clickonsearch/linkedin-mcp-server 0.1.1 → 0.2.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 +18 -1
- package/dist/access.d.ts +16 -0
- package/dist/access.js +40 -0
- package/dist/access.js.map +1 -0
- package/dist/auth.js +4 -0
- package/dist/auth.js.map +1 -1
- package/dist/cli.js +18 -16
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +7 -1
- package/dist/server.js +11 -2
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,9 +52,26 @@ LINKEDIN_CLIENT_ID=<id> LINKEDIN_CLIENT_SECRET=<secret> \
|
|
|
52
52
|
npx @clickonsearch/linkedin-mcp-server
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
It's now listening on `http://
|
|
55
|
+
It's now listening on `http://127.0.0.1:4300` for whatever agent you point
|
|
56
56
|
at it.
|
|
57
57
|
|
|
58
|
+
### Letting another machine connect
|
|
59
|
+
|
|
60
|
+
By default only programs on the same computer can reach this server. To
|
|
61
|
+
allow others (an agent in a container, or on another machine), pick a
|
|
62
|
+
secret and set both:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
export LINKEDIN_MCP_TOKEN=$(openssl rand -hex 32)
|
|
66
|
+
BIND_HOST=0.0.0.0 npx @clickonsearch/linkedin-mcp-server
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Give `linkedin-agent` the same `LINKEDIN_MCP_TOKEN` and it sends it
|
|
70
|
+
automatically. The server refuses to start on any other address without a
|
|
71
|
+
token. Anyone holding the token can post to your LinkedIn feed as you, so
|
|
72
|
+
treat it like a password — and since it travels over plain HTTP, only use
|
|
73
|
+
this on a network you trust or behind TLS (for example a reverse proxy).
|
|
74
|
+
|
|
58
75
|
## What it exposes
|
|
59
76
|
|
|
60
77
|
| Tool | What it does |
|
package/dist/access.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { RequestHandler } from "express";
|
|
2
|
+
export interface ListenConfig {
|
|
3
|
+
host: string;
|
|
4
|
+
/** Bearer token every caller must present. Always set when `host` isn't loopback. */
|
|
5
|
+
authToken?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function isLoopbackHost(host: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Listens on loopback only unless BIND_HOST says otherwise, and refuses to
|
|
10
|
+
* listen on anything else without a token — the MCP tools can publish
|
|
11
|
+
* posts to your LinkedIn feed as you, so a network-reachable server with no
|
|
12
|
+
* caller authentication is never acceptable.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveListenConfig(env?: NodeJS.ProcessEnv): ListenConfig;
|
|
15
|
+
/** Rejects any request without `Authorization: Bearer <token>`. Compared in constant time. */
|
|
16
|
+
export declare function requireBearerToken(token: string): RequestHandler;
|
package/dist/access.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
const LOOPBACK_HOSTS = ["127.0.0.1", "localhost", "::1"];
|
|
3
|
+
const MIN_TOKEN_LENGTH = 16;
|
|
4
|
+
export function isLoopbackHost(host) {
|
|
5
|
+
return LOOPBACK_HOSTS.includes(host);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Listens on loopback only unless BIND_HOST says otherwise, and refuses to
|
|
9
|
+
* listen on anything else without a token — the MCP tools can publish
|
|
10
|
+
* posts to your LinkedIn feed as you, so a network-reachable server with no
|
|
11
|
+
* caller authentication is never acceptable.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveListenConfig(env = process.env) {
|
|
14
|
+
const host = env.BIND_HOST || "127.0.0.1";
|
|
15
|
+
const authToken = env.LINKEDIN_MCP_TOKEN || undefined;
|
|
16
|
+
if (authToken && authToken.length < MIN_TOKEN_LENGTH) {
|
|
17
|
+
throw new Error(`LINKEDIN_MCP_TOKEN must be at least ${MIN_TOKEN_LENGTH} characters (try: openssl rand -hex 32)`);
|
|
18
|
+
}
|
|
19
|
+
if (!isLoopbackHost(host) && !authToken) {
|
|
20
|
+
throw new Error(`Refusing to listen on ${host} without authentication. Set LINKEDIN_MCP_TOKEN ` +
|
|
21
|
+
`(try: openssl rand -hex 32), or unset BIND_HOST to listen on 127.0.0.1 only.`);
|
|
22
|
+
}
|
|
23
|
+
return { host, authToken };
|
|
24
|
+
}
|
|
25
|
+
function sha256(value) {
|
|
26
|
+
return createHash("sha256").update(value).digest();
|
|
27
|
+
}
|
|
28
|
+
/** Rejects any request without `Authorization: Bearer <token>`. Compared in constant time. */
|
|
29
|
+
export function requireBearerToken(token) {
|
|
30
|
+
const expected = sha256(token);
|
|
31
|
+
return (req, res, next) => {
|
|
32
|
+
const match = /^Bearer\s+(.+)$/i.exec(req.headers.authorization ?? "");
|
|
33
|
+
if (match && timingSafeEqual(sha256(match[1]), expected)) {
|
|
34
|
+
next();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
res.status(401).set("WWW-Authenticate", "Bearer").json({ error: "unauthorized" });
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=access.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access.js","sourceRoot":"","sources":["../src/access.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG1D,MAAM,cAAc,GAAG,CAAC,WAAW,EAAE,WAAW,EAAE,KAAK,CAAC,CAAC;AACzD,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAQ5B,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,cAAc,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AACvC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACtE,MAAM,IAAI,GAAG,GAAG,CAAC,SAAS,IAAI,WAAW,CAAC;IAC1C,MAAM,SAAS,GAAG,GAAG,CAAC,kBAAkB,IAAI,SAAS,CAAC;IAEtD,IAAI,SAAS,IAAI,SAAS,CAAC,MAAM,GAAG,gBAAgB,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CAAC,uCAAuC,gBAAgB,yCAAyC,CAAC,CAAC;IACpH,CAAC;IACD,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CACb,yBAAyB,IAAI,kDAAkD;YAC7E,8EAA8E,CACjF,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;AAC7B,CAAC;AAED,SAAS,MAAM,CAAC,KAAa;IAC3B,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC;AACrD,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAE/B,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACxB,MAAM,KAAK,GAAG,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC;QACvE,IAAI,KAAK,IAAI,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,EAAE,CAAC;YACzD,IAAI,EAAE,CAAC;YACP,OAAO;QACT,CAAC;QACD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,kBAAkB,EAAE,QAAQ,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAC;IACpF,CAAC,CAAC;AACJ,CAAC"}
|
package/dist/auth.js
CHANGED
|
@@ -85,6 +85,10 @@ export class TokenManager {
|
|
|
85
85
|
throw new Error("LinkedIn access token has expired. LinkedIn only issues refresh tokens to apps approved for " +
|
|
86
86
|
'offline access; without one, re-run "npm run authorize" to get a new token.');
|
|
87
87
|
}
|
|
88
|
+
if (!this.config.clientId || !this.config.clientSecret) {
|
|
89
|
+
throw new Error("LinkedIn access token needs refreshing, but LINKEDIN_CLIENT_ID/LINKEDIN_CLIENT_SECRET aren't " +
|
|
90
|
+
'set. Add them to your .env, or re-run "npm run authorize" to get a fresh token.');
|
|
91
|
+
}
|
|
88
92
|
this.token = await refreshAccessToken(this.config, this.token.refreshToken);
|
|
89
93
|
await saveToken(this.tokenPath, this.token);
|
|
90
94
|
}
|
package/dist/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -3,40 +3,42 @@ import "dotenv/config";
|
|
|
3
3
|
import { TokenManager } from "./auth.js";
|
|
4
4
|
import { LinkedInClient } from "./linkedin.js";
|
|
5
5
|
import { createServer } from "./server.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
6
|
+
import { resolveListenConfig } from "./access.js";
|
|
7
|
+
/**
|
|
8
|
+
* clientId/clientSecret are only needed to refresh an expiring token (see
|
|
9
|
+
* TokenManager.getValidAccessToken) — a still-valid saved token needs
|
|
10
|
+
* neither, so this intentionally doesn't require them at startup.
|
|
11
|
+
*/
|
|
13
12
|
function loadConfig() {
|
|
14
13
|
return {
|
|
15
|
-
clientId:
|
|
16
|
-
clientSecret:
|
|
14
|
+
clientId: process.env.LINKEDIN_CLIENT_ID ?? "",
|
|
15
|
+
clientSecret: process.env.LINKEDIN_CLIENT_SECRET ?? "",
|
|
17
16
|
redirectUri: process.env.LINKEDIN_REDIRECT_URI ?? "http://localhost:3300/callback",
|
|
18
17
|
scopes: (process.env.LINKEDIN_SCOPES ?? "openid profile email w_member_social").split(" "),
|
|
19
18
|
};
|
|
20
19
|
}
|
|
21
20
|
async function main() {
|
|
22
|
-
let
|
|
21
|
+
let listen;
|
|
23
22
|
try {
|
|
24
|
-
|
|
23
|
+
listen = resolveListenConfig();
|
|
25
24
|
}
|
|
26
25
|
catch (error) {
|
|
27
26
|
console.error(error instanceof Error ? error.message : String(error));
|
|
28
27
|
process.exitCode = 1;
|
|
29
28
|
return;
|
|
30
29
|
}
|
|
30
|
+
const config = loadConfig();
|
|
31
31
|
const tokenPath = process.env.LINKEDIN_TOKEN_PATH ?? "./linkedin-token.json";
|
|
32
32
|
const port = Number(process.env.PORT ?? 4300);
|
|
33
33
|
const tokens = new TokenManager(config, tokenPath);
|
|
34
34
|
const linkedin = new LinkedInClient(tokens);
|
|
35
|
-
const app = createServer(linkedin);
|
|
36
|
-
app.listen(port, () => {
|
|
37
|
-
|
|
38
|
-
console.error(`
|
|
39
|
-
console.error(`
|
|
35
|
+
const app = createServer(linkedin, listen);
|
|
36
|
+
app.listen(port, listen.host, () => {
|
|
37
|
+
const origin = `http://${listen.host.includes(":") ? `[${listen.host}]` : listen.host}:${port}`;
|
|
38
|
+
console.error(`linkedin-mcp-server listening on ${origin}`);
|
|
39
|
+
console.error(` MCP endpoint: POST ${origin}/mcp`);
|
|
40
|
+
console.error(` Health: GET ${origin}/health`);
|
|
41
|
+
console.error(listen.authToken ? " Auth: bearer token required (LINKEDIN_MCP_TOKEN)" : " Auth: none (loopback only)");
|
|
40
42
|
console.error(` If tools fail with "Not authorized yet", run: npm run authorize`);
|
|
41
43
|
});
|
|
42
44
|
}
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +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;
|
|
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;AAC3C,OAAO,EAAE,mBAAmB,EAAqB,MAAM,aAAa,CAAC;AAErE;;;;GAIG;AACH,SAAS,UAAU;IACjB,OAAO;QACL,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,EAAE;QAC9C,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,EAAE;QACtD,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,MAAoB,CAAC;IACzB,IAAI,CAAC;QACH,MAAM,GAAG,mBAAmB,EAAE,CAAC;IACjC,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,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,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,EAAE,MAAM,CAAC,CAAC;IAE3C,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;QACjC,MAAM,MAAM,GAAG,UAAU,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC;QAChG,OAAO,CAAC,KAAK,CAAC,oCAAoC,MAAM,EAAE,CAAC,CAAC;QAC5D,OAAO,CAAC,KAAK,CAAC,wBAAwB,MAAM,MAAM,CAAC,CAAC;QACpD,OAAO,CAAC,KAAK,CAAC,wBAAwB,MAAM,SAAS,CAAC,CAAC;QACvD,OAAO,CAAC,KAAK,CACX,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,4DAA4D,CAAC,CAAC,CAAC,sCAAsC,CACzH,CAAC;QACF,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
CHANGED
|
@@ -3,3 +3,5 @@ export type { LinkedInOAuthConfig, StoredToken } from "./auth.js";
|
|
|
3
3
|
export { LinkedInClient } from "./linkedin.js";
|
|
4
4
|
export type { LinkedInProfile } from "./linkedin.js";
|
|
5
5
|
export { createServer } from "./server.js";
|
|
6
|
+
export { resolveListenConfig, requireBearerToken, isLoopbackHost } from "./access.js";
|
|
7
|
+
export type { ListenConfig } from "./access.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { TokenManager, buildAuthorizationUrl, exchangeCodeForToken, refreshAccessToken, loadToken, saveToken } from "./auth.js";
|
|
2
2
|
export { LinkedInClient } from "./linkedin.js";
|
|
3
3
|
export { createServer } from "./server.js";
|
|
4
|
+
export { resolveListenConfig, requireBearerToken, isLoopbackHost } from "./access.js";
|
|
4
5
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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"}
|
|
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;AAC3C,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { Express } from "express";
|
|
2
2
|
import type { LinkedInClient } from "./linkedin.js";
|
|
3
|
+
import { type ListenConfig } from "./access.js";
|
|
3
4
|
/**
|
|
4
5
|
* Builds the Express app: `POST /mcp` is the stateless MCP endpoint (a
|
|
5
6
|
* fresh McpServer per request, matching the SDK's own reference server),
|
|
6
7
|
* `GET /health` is a liveness check. Unlike whatsapp-mcp-server, there's no
|
|
7
8
|
* push/events stream here — LinkedIn's API is pure request/response, there's
|
|
8
9
|
* nothing to watch for.
|
|
10
|
+
*
|
|
11
|
+
* `listen` must match what the caller passes to `app.listen`. The SDK's Host
|
|
12
|
+
* header check only guards browser-origin (DNS rebinding) attacks and is
|
|
13
|
+
* trivially bypassed by a network client sending `Host: localhost`, so it
|
|
14
|
+
* is not authentication: `authToken`, when set, gates every route.
|
|
9
15
|
*/
|
|
10
|
-
export declare function createServer(linkedin: LinkedInClient): Express;
|
|
16
|
+
export declare function createServer(linkedin: LinkedInClient, listen?: ListenConfig): Express;
|
package/dist/server.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as z from "zod/v4";
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
4
|
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
5
|
+
import { requireBearerToken } from "./access.js";
|
|
5
6
|
function buildMcpServer(linkedin) {
|
|
6
7
|
const server = new McpServer({ name: "linkedin-mcp-server", version: "0.1.0" }, { capabilities: {} });
|
|
7
8
|
server.registerTool("get_profile", { description: "Get the authenticated LinkedIn member's basic profile (name, email, picture)", inputSchema: {} }, async () => {
|
|
@@ -23,9 +24,17 @@ function buildMcpServer(linkedin) {
|
|
|
23
24
|
* `GET /health` is a liveness check. Unlike whatsapp-mcp-server, there's no
|
|
24
25
|
* push/events stream here — LinkedIn's API is pure request/response, there's
|
|
25
26
|
* nothing to watch for.
|
|
27
|
+
*
|
|
28
|
+
* `listen` must match what the caller passes to `app.listen`. The SDK's Host
|
|
29
|
+
* header check only guards browser-origin (DNS rebinding) attacks and is
|
|
30
|
+
* trivially bypassed by a network client sending `Host: localhost`, so it
|
|
31
|
+
* is not authentication: `authToken`, when set, gates every route.
|
|
26
32
|
*/
|
|
27
|
-
export function createServer(linkedin) {
|
|
28
|
-
const app = createMcpExpressApp();
|
|
33
|
+
export function createServer(linkedin, listen = { host: "127.0.0.1" }) {
|
|
34
|
+
const app = createMcpExpressApp({ host: listen.host });
|
|
35
|
+
if (listen.authToken) {
|
|
36
|
+
app.use(requireBearerToken(listen.authToken));
|
|
37
|
+
}
|
|
29
38
|
app.post("/mcp", async (req, res) => {
|
|
30
39
|
try {
|
|
31
40
|
const server = buildMcpServer(linkedin);
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AACnG,OAAO,EAAE,mBAAmB,EAAE,MAAM,6CAA6C,CAAC;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clickonsearch/linkedin-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|