Skip to main content
Glama
2060-io

linkedin-mcp

by 2060-io
README.md
# linkedin-mcp

An MCP (Model Context Protocol) server that lets AI agents **publish to LinkedIn** as
the authenticated member — text posts, link shares, single/multi-image posts, reshares,
comments, and reactions — with built-in safety rails for autonomous operation: daily
budget limits, engagement deduplication, compact TOON-encoded responses, and self-describing errors with typo suggestions.

Authentication is **OAuth 2.0 (3-legged)** with a **cluster-hosted callback** protected
by HTTP Basic Auth, so you never have to copy tokens onto a server by hand. Tokens,
budgets, and dedup state live in a single JSON file you can back with a Kubernetes PVC.

Works with **Claude Code**, **Claude Desktop**, **OpenAI Codex**, **Cursor**,
**Windsurf**, **Cline**, and any other MCP-compatible client — run it locally or via Docker.

> **Scope (Phase 1).** LinkedIn's public API only allows **member write** actions plus
> reading your own profile. There is **no** public API to read the feed, search posts,
> list followers/connections, or follow people. This MCP is therefore publish-and-engage
> oriented. See [`spec.md`](./spec.md) for the full design and rationale.

---

## What Can It Do?

| Category | Tools | What You Can Say |
| --- | --- | --- |
| **Identity** | `get_me` | "Who am I posting as?" |
| **Publish** | `create_post`, `create_image_post`, `create_multi_image_post`, `reshare_post` | "Post 'hello world' on LinkedIn" / "Share this image with a caption" |
| **Media** | `upload_media` | "Upload this image and post it" |
| **Engage** | `comment_on_post`, `react_to_post` | "Comment 'congrats' on this post" / "Like this post" |
| **Lifecycle** | `delete_post` | "Delete that post" |

Post-targeting tools accept either a LinkedIn **URN** (`urn:li:share:...` /
`urn:li:ugcPost:...` / `urn:li:activity:...`) or a post **URL** interchangeably.

---

## Safety Features

### Daily budget limits

Hard per-action limits per day. The server refuses when exhausted — even if the LLM
ignores every instruction.

```bash
LI_MCP_MAX_POSTS=5       # Max posts/reshares per day (default)
LI_MCP_MAX_COMMENTS=10   # Max comments per day
LI_MCP_MAX_REACTIONS=30  # Max reactions per day
LI_MCP_MAX_DELETES=3     # Max post deletions per day
```

Set to `0` to disable an action entirely. Set to `-1` for unlimited.

### Budget counters in every response

Every response includes the remaining budget, so the LLM sees its limits proactively:

```text
li_budget: "1/5 posts used, 0/10 comments used, 2/30 reactions used, 0/3 deletes used | last action: 3m ago"
```

### Engagement deduplication (default on)

Never comment on or react to the same post twice. Set `LI_MCP_DEDUP=false` to disable.

### TOON + compact responses (default on)

Responses use [TOON](https://github.com/toon-format/toon) and drop verbose fields to
save tokens. Set `LI_MCP_TOON=false` for JSON, `LI_MCP_COMPACT=false` to keep full shapes.

### Self-describing errors

Tools validate parameters and return actionable hints with fuzzy-matched suggestions for
typos, so the LLM learns from mistakes instead of getting opaque errors.

### Text formatting (automatic)

You pass post text as **plain text** — the server handles LinkedIn's quirks for you:

- LinkedIn parses `commentary` as **"Little Text"**, where `\ | { } @ [ ] ( ) < > # * _ ~`
  are reserved. An unescaped one (notably `(`) **silently truncates** the post. The server
  escapes them automatically.
- `#hashtags` are preserved as clickable hashtags.
- Images are only attached once LinkedIn finishes processing them, so posts never render as
  "This post cannot be displayed".

---

## Setup

### 1. Clone, install, build

```bash
git clone https://github.com/2060-io/linkedin-mcp.git
cd linkedin-mcp
npm install
npm run build
```

### 2. Create a LinkedIn app

1. Go to the [LinkedIn Developer Portal](https://www.linkedin.com/developers/apps) and
   create an app (it must be linked to a Company Page).
2. On the **Products** tab, request **Sign In with LinkedIn using OpenID Connect** and
   **Share on LinkedIn**. This grants the `openid`, `profile`, `email`, and
   `w_member_social` scopes.
3. On the **Auth** tab, copy the **Client ID** and **Client Secret**, and add your
   **Redirect URL** (see below).

### 3. Configure credentials

```bash
cp .env.example .env
```

Fill in `LINKEDIN_CLIENT_ID`, `LINKEDIN_CLIENT_SECRET`, and `LINKEDIN_REDIRECT_URI`
(must EXACTLY match a Redirect URL on the app). See [`.env.example`](./.env.example) for
every option.

---

## Authorize (get a member token)

OAuth tokens are minted once, then refreshed automatically until the refresh token
expires (~1 year), at which point you re-authorize.

### Option A — Local dev CLI

```bash
# LINKEDIN_REDIRECT_URI=http://localhost:8000/oauth/callback
npm run auth
```

Open the printed URL, approve, and the tokens are written to the local state file.

### Option B — Cluster-hosted callback (recommended for k8s)

With `MCP_TRANSPORT=http` and `LI_MCP_ADMIN_USER` / `LI_MCP_ADMIN_PASSWORD` set, the
server exposes Basic-Auth-protected OAuth routes:

```text
GET /oauth/start     # redirects to LinkedIn consent
GET /oauth/callback  # LinkedIn returns here; tokens persisted to state file
GET /oauth/status    # shows whether a valid token is stored
```

Visit `https://<public-host>/oauth/start`, authenticate with Basic Auth, approve on
LinkedIn, and the member tokens land on the PVC. See [`charts/README.md`](./charts/README.md).

---

## Run

```bash
# stdio (for local MCP clients)
MCP_TRANSPORT=stdio npm start

# http (for cluster / remote)
MCP_TRANSPORT=http MCP_PORT=8000 npm start
```

HTTP endpoints: `/mcp` (StreamableHTTP), `/healthz`, and `/oauth/*`.

### Connect a client (stdio example)

```json
{
  "mcpServers": {
    "linkedin": {
      "command": "node",
      "args": ["/absolute/path/to/linkedin-mcp/dist/index.js"],
      "env": { "MCP_TRANSPORT": "stdio" }
    }
  }
}
```

---

## Deploy to Kubernetes

A Helm chart is provided in [`charts/`](./charts). It provisions a PVC for token/budget
state, runs a single replica with the `Recreate` strategy, and wires LinkedIn + Basic
Auth credentials through a Secret. See [`charts/README.md`](./charts/README.md) for
install and authorization steps.

---

## State persistence

All durable state — OAuth tokens (the source of truth), daily budget counters, and
engagement dedup — lives in one JSON file (`LI_MCP_STATE_FILE`, default
`{cwd}/linkedin-mcp-state.json`). On Kubernetes, back it with a PVC so tokens survive
restarts; otherwise a restart forces re-authorization and resets budgets.

---

## Development

```bash
npm run build   # tsc
npm test        # vitest
npm run dev     # build + start
```

---

## Credits

TOON encoder vendored from [@toon-format/toon](https://github.com/toon-format/toon)
(MIT). Architecture mirrors the sibling
[x-autonomous-mcp](https://github.com/2060-io/x-autonomous-mcp).

## License

MIT

TDQS

A3.7/5.0

Scored across 9 tools

Disambiguation5/5

Each tool targets a distinct action (comment, create different post types, delete, react, reshare, upload, profile fetch) with no overlap in functionality.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern in snake_case, such as comment_on_post, create_image_post, and react_to_post.

Tool Count5/5

9 tools cover the core LinkedIn posting and engagement actions without being excessive or insufficient.

Completeness4/5

Covers creation, deletion, commenting, reacting, and resharing posts, but lacks retrieval tools like get_post or list_posts, which is a minor gap.

Maintenance

ActivityStale
ResponsivenessNo issues