Skip to main content
Glama
aneesh-a7

linkedin-mcp

by aneesh-a7
README.md
# linkedin-mcp

A personal-use remote MCP server, hosted on Cloudflare Workers, that lets you
post to LinkedIn, queue/schedule drafts, and pull your own post analytics
from Claude. Single-user (your own LinkedIn account only).

See `PROGRESS.md` for build status and known API limitations.

## Tools exposed

- `create_post(text, visibility)` — publish now
- `save_draft(text, scheduled_time, visibility)` — queue for later, published by a cron trigger
- `list_drafts()` — see queued/scheduled/published drafts
- `delete_post(post_id)` — cancel a draft, or delete a published LinkedIn post (accepts a draft id or a LinkedIn post URN)
- `get_recent_posts(count)` — your last N posts
- `get_post_analytics(post_id)` — engagement counts for one post (see limitations below)
- `get_profile_stats()` — basic profile info (see limitations below)

## Architecture

- **Hosting:** Cloudflare Workers, MCP over Streamable HTTP at `/mcp`, via the
  `agents` package's `McpAgent` (a Durable Object).
- **Storage:** Cloudflare D1 — one table for your OAuth token pair, one table
  for the draft/schedule queue.
- **Scheduling:** a Cron Trigger (every 15 min) checks D1 for due drafts and
  publishes them.
- **Auth to LinkedIn:** OAuth 2.0 3-legged flow, `w_member_social` +
  `openid`/`profile` scopes. Tokens are refreshed automatically using the
  stored refresh_token.
- **Auth to this server:** the `/mcp` endpoint requires
  `Authorization: Bearer <MCP_AUTH_TOKEN>` — without this, anyone who finds
  your `*.workers.dev` URL could post to your LinkedIn as you.

## Setup

### 1. Register a LinkedIn app

1. Go to https://www.linkedin.com/developers/apps and create an app.
2. Under **Products**, request/add:
   - **Share on LinkedIn** (gives `w_member_social`)
   - **Sign In with LinkedIn using OpenID Connect** (gives `openid`, `profile`)
3. Under **Auth**, note your **Client ID** and **Client Secret**. You'll add
   the redirect URL here after step 4 gives you your Workers URL.

### 2. Cloudflare setup

```bash
npm install
npx wrangler login
npx wrangler d1 create linkedin_mcp_db
```

Copy the `database_id` from the output into `wrangler.jsonc`
(`d1_databases[0].database_id`).

Generate a random bearer token for securing your own `/mcp` endpoint, e.g.:

```bash
openssl rand -hex 32
```

Set secrets:

```bash
npx wrangler secret put LINKEDIN_CLIENT_ID
npx wrangler secret put LINKEDIN_CLIENT_SECRET
npx wrangler secret put MCP_AUTH_TOKEN
```

### 3. Set your redirect URI

Deploy once to learn your `*.workers.dev` URL:

```bash
npm run deploy
```

Update `wrangler.jsonc`'s `vars.LINKEDIN_REDIRECT_URI` to
`https://<your-subdomain>.<your-account>.workers.dev/oauth/callback`, then add
that exact URL as an **Authorized redirect URL** in the LinkedIn app's Auth
settings. Redeploy:

```bash
npm run deploy
```

### 4. Run the D1 migration

```bash
npm run db:migrate:remote
```

### 5. One-time LinkedIn login

In a browser, visit:

```
https://<your-worker>/oauth/authorize?token=<MCP_AUTH_TOKEN>
```

Log in with your own LinkedIn account and approve. You'll see a "connected
successfully" page. This stores your access/refresh token pair in D1; the
worker refreshes it automatically going forward.

### 6. Connect from Claude

Add a remote MCP connector pointing at `https://<your-worker>/mcp` with an
`Authorization: Bearer <MCP_AUTH_TOKEN>` header. Ask Claude to run
`create_post` with a short test message to confirm everything end-to-end.

## Local development

```bash
cp .dev.vars.example .dev.vars   # fill in real values
npm run db:migrate:local
npm run dev
```

`wrangler dev` runs a local D1 replica; you'll still hit the real LinkedIn API,
so use a low-visibility test post the first few times.

## Non-goals

No scraping/reading other people's profiles, no auto-connect/DM/engagement-pod
automation, no multi-user OAuth (single LinkedIn account only).