Claude LinkedIn Studio
by madalincmc
README.md
<div align="center">
<img src="docs/images/hero.svg" alt="Claude LinkedIn Studio: an idea in Claude becomes a published LinkedIn post" width="100%">
# Claude LinkedIn Studio
**Write, polish and publish LinkedIn posts without leaving Claude.**
Share an idea in a chat. Claude shapes it into a post in your voice, checks it against your real experience, and publishes it to your LinkedIn profile when you say so.
[](LICENSE)
[](https://nextjs.org)
[](https://modelcontextprotocol.io)
[](https://vercel.com)
[What you can do](#what-you-can-do) · [How it works](#how-it-works) · [Setup](#setup) · [Using it](#using-it) · [Security](#security) · [FAQ](#faq)
</div>
---
<p align="center">
<img src="docs/images/idea-to-post.svg" alt="A Claude chat turning an idea into a draft and publishing it, next to the resulting LinkedIn post" width="100%">
</p>
## What you can do
- 💡 **Share ideas straight from Claude.** A rough thought, a few bullets, a voice-note transcript, a link you're reacting to. Drop it into a chat from your desktop or phone.
- ✍️ **Polish them with Claude.** Get one focused draft, give feedback ("shorter", "less formal", "lead with the second point"), and iterate until it's right.
- 🎙️ **Sound like yourself.** Your role, opinions, writing voice, real stories and boundaries live in five reference files plus Project instructions. Claude writes from them, and can interview you to fill them in.
- 🛡️ **Never post invented experience.** Claude follows strict authenticity rules: no made-up anecdotes, results, numbers or opinions. When a story is missing, it asks for one instead of making it up.
- 🔎 **Research topics.** Ask Claude to look into what's new in your field and draft a post from it, with sources listed and facts attributed.
- 🗓️ **Schedule and automate.** Set up recurring tasks, for example "every Monday, research my topics and draft one post for review", or go fully hands-off.
- 🚀 **Publish in one sentence.** Say "publish it" and the exact approved text goes to your LinkedIn feed. Delete a post the same way.
## How it works
<p align="center">
<img src="docs/images/workflow.svg" alt="Workflow: share an idea, polish it, your voice, research and schedule, publish" width="100%">
</p>
```mermaid
flowchart LR
You(["You"]) -- "ideas, feedback,<br/>'publish it'" --> Claude
subgraph Claude["Claude Project"]
I["Instructions<br/>(authenticity rules)"]
K["Your 5 reference files<br/>(identity, opinions, voice,<br/>stories, boundaries)"]
end
Claude -- "MCP tools<br/>publish_post · delete_post · linkedin_status" --> App
subgraph App["Your deployment (Vercel)"]
M["/api/mcp<br/>OAuth 2.1 or bearer token"]
DB[("Neon Postgres<br/>encrypted LinkedIn token,<br/>publish log")]
C["Console<br/>(Google sign-in)"]
end
App -- "Posts API" --> LI["LinkedIn"]
```
There are two halves:
1. **The Claude side** is a Claude Project that holds your instructions and reference files. Drafting, feedback, research and scheduling all happen there. No extra AI API keys and no per-token bills from this project.
2. **The connector** is a small Next.js app that you deploy to your own Vercel account. It holds your LinkedIn connection (encrypted) and exposes a remote [MCP](https://modelcontextprotocol.io) server with three tools:
| Tool | What it does |
|---|---|
| `linkedin_status` | Shows whether LinkedIn is connected, which account, and when access expires. |
| `publish_post` | Publishes the exact approved text to your feed (public or connections only). Accidental double posts are blocked. |
| `delete_post` | Deletes a post that was published through the connector. |
Everything runs on your own accounts: your Vercel project, your database, your Google sign-in, your LinkedIn app. Nobody else, including the author of this repo, can see your data or post for you.
## Setup
Plan on about **45 minutes** the first time. Every step is concrete. Where a value is specific to you, it's written as `<like-this>`.
### What you need
| Account / tool | Why | Cost |
|---|---|---|
| [GitHub](https://github.com) | To fork the repo and deploy from it | Free |
| [Vercel](https://vercel.com) | Hosts the connector | Hobby plan is free |
| [Neon](https://neon.tech) (through the Vercel Marketplace) | Postgres database | Free plan |
| [Google Cloud](https://console.cloud.google.com) | "Sign in with Google" for your console | Free |
| [LinkedIn developer app](https://www.linkedin.com/developers/apps) | Permission to post as you | Free, but requires a LinkedIn Page (step 6) |
| [Claude](https://claude.ai) | Drafting and publishing. Projects and custom connectors are needed; see Anthropic's help center for current plan availability. | Your Claude plan |
| Node.js 22+ and the [Vercel CLI](https://vercel.com/docs/cli) | Local commands: `npm i -g vercel` | Free |
### 1. Fork and clone
1. Click **Fork** at the top of this page. Your copy can be private.
2. Clone your fork and install dependencies:
```bash
git clone https://github.com/<your-github-user>/claude-linkedin-studio.git
cd claude-linkedin-studio
npm install
```
### 2. Deploy to Vercel
You deploy first because you need your app's URL for the Google and LinkedIn setup.
1. Go to [vercel.com/new](https://vercel.com/new), import your fork and click **Deploy**. The defaults are correct for this Next.js app. The first deploy builds fine without any configuration.
2. Note your production URL, for example `https://<your-project>.vercel.app`. The rest of this guide calls it **`<APP_URL>`**.
3. Link your local folder to the project:
```bash
vercel link
```
### 3. Add the database
1. In the Vercel dashboard, open your project and go to **Storage → Create Database → Neon (Serverless Postgres)**. Choose the free plan and a region near you, and connect it to **all environments**.
2. Vercel adds `DATABASE_URL` (pooled) and `DATABASE_URL_UNPOOLED` (direct) to your project automatically.
3. Pull them locally and create the tables:
```bash
vercel env pull .env.local
npm run db:migrate
```
You should see `migrations applied successfully`.
### 4. Generate your secrets
Run these three commands and keep the output somewhere safe, like a password manager. You'll paste the values in step 7.
```bash
# SESSION_SECRET: signs your console session cookie
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
# TOKEN_ENCRYPTION_KEY: encrypts your LinkedIn token in the database
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
# MCP_TOKEN: optional bearer token for Claude Code
node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
```
### 5. Create the Google sign-in client
The console is protected by Google sign-in and accepts exactly one account: yours.
1. Open the [Google Cloud console](https://console.cloud.google.com), click the project picker and choose **New project**. Name it, for example, `claude-linkedin-studio`.
2. Go to **Google Auth Platform**. If you're asked to configure it, click **Get started** and fill in:
- **App name**: anything, e.g. `Claude LinkedIn Studio`
- **User support email**: your email
- **Audience**: **External**
- **Contact email**: your email
3. Open **Audience → Test users → Add users** and add the Google account you'll sign in with. The app can stay in **Testing** mode. You don't need to publish it or get it verified.
4. Open **Clients → Create client**:
- **Application type**: **Web application**
- **Authorized redirect URIs**:
- `<APP_URL>/api/auth/google/callback`
- `http://localhost:3000/api/auth/google/callback` (for local development)
5. Click **Create**, then copy the **Client ID** and the **Client secret**. Google shows the secret only once.
### 6. Create the LinkedIn app
1. LinkedIn requires every developer app to be associated with a **LinkedIn Page**. If you don't have one, create one at [linkedin.com/company/setup/new](https://www.linkedin.com/company/setup/new). A simple page for yourself or a side project is enough.
2. Go to [linkedin.com/developers/apps](https://www.linkedin.com/developers/apps) → **Create app**. Enter a name, select your Page, upload a logo and accept the terms.
3. If LinkedIn asks you to verify the app with your Page, open **Settings → Verify** and approve the request as the Page admin.
4. Open the **Products** tab and request access to both of these. They're self-serve and usually approved instantly:
- **Sign In with LinkedIn using OpenID Connect**
- **Share on LinkedIn**
5. Open the **Auth** tab:
- Under **Authorized redirect URLs for your app**, add:
- `<APP_URL>/api/linkedin/callback`
- `http://localhost:3000/api/linkedin/callback`
- Copy the **Client ID** and the **Primary Client Secret**.
- Under **OAuth 2.0 scopes**, check that `openid`, `profile` and `w_member_social` are listed.
### 7. Configure environment variables
In the Vercel dashboard, open **Project → Settings → Environment Variables**. Add each variable below and tick both **Production** and **Development**, so local development uses the same values. Keep the default type: values are encrypted at rest. Vercel's *Sensitive* type can't be pulled for local development, so use it only for Production values you don't need locally.
| Variable | Value |
|---|---|
| `SESSION_SECRET` | From step 4 |
| `TOKEN_ENCRYPTION_KEY` | From step 4 |
| `MCP_TOKEN` | From step 4 (optional; only for Claude Code) |
| `GOOGLE_CLIENT_ID` | From step 5 |
| `GOOGLE_CLIENT_SECRET` | From step 5 |
| `OWNER_EMAIL` | The Google account allowed to sign in, e.g. `<you@gmail.com>` |
| `LINKEDIN_CLIENT_ID` | From step 6 |
| `LINKEDIN_CLIENT_SECRET` | From step 6 |
<details>
<summary>Prefer the CLI?</summary>
```bash
vercel env add SESSION_SECRET production
vercel env add SESSION_SECRET development
# …repeat for each variable. The CLI prompts for the value, so it doesn't end up in your shell history.
```
</details>
Then apply the changes:
1. Redeploy: **Deployments → ⋯ on the latest deployment → Redeploy**.
2. Refresh your local env file with `vercel env pull .env.local`.
### 8. Sign in and connect LinkedIn
1. Open `<APP_URL>` and click **Sign in with Google**. Use the account from `OWNER_EMAIL`.
2. Click **Connect LinkedIn** and approve the access request.
3. The console now shows **Connected as `<your name>`** and when access expires.
### 9. Connect Claude
**claude.ai (web, desktop and mobile)**
1. Open **Settings → Connectors → Add custom connector**.
2. Enter any name, e.g. `LinkedIn`, and the URL `<APP_URL>/api/mcp`.
3. Claude sends you to your console's consent screen. Sign in with Google if asked, then click **Allow**.
4. In a new chat, open the tools menu and make sure the connector is enabled. Ask *"What's my LinkedIn status?"* to test it.
**Claude Code (optional)**
```bash
claude mcp add --transport http linkedin <APP_URL>/api/mcp \
--header "Authorization: Bearer <MCP_TOKEN>"
```
Your console's **Claude setup** section shows both options with your real URL filled in.
### 10. Create your Claude Project and set your voice
1. In claude.ai, go to **Projects → New project**, for example `LinkedIn`.
2. Paste the full contents of [`claude-workspace/instructions.md`](claude-workspace/instructions.md) into the Project **Instructions**.
3. Upload the five files from [`claude-workspace/knowledge/`](claude-workspace/knowledge) to the Project's knowledge (shown as **Files** or **Context**, depending on your Claude version).
4. Start a chat in the Project and say **"Interview me"**. Claude asks a few questions per file and gives you the filled-in content to replace each template. Or edit the files yourself; each one explains what goes where.
5. Optional: run the checks in [`claude-workspace/test-prompts.md`](claude-workspace/test-prompts.md) to confirm Claude doesn't invent anything about you.
### 11. Publish your first post
In a new chat in the Project:
> **You:** Idea: most "AI transformation" projects fail on change management, not on the model. Short insight.
>
> **Claude:** *(a draft in your voice)*
>
> **You:** Shorter, and drop the last line.
>
> **Claude:** *(revised draft)*
>
> **You:** Publish it, connections only.
>
> **Claude:** Published: `https://www.linkedin.com/feed/update/urn:li:share:…`
That's it. The post shows up in your console's **Publish history**.
## Using it
### Everyday flow
| You say | Claude does |
|---|---|
| *"Here's a thought: …"* | Drafts one post around your point, in your voice and language |
| *"Opinion, provocative but respectful"* | Changes type and tone (types: Short Insight, Opinion, Educational, Lesson Learned, Question; lengths: Short, Medium, Long) |
| *"Less formal, no emojis"* | Revises, and asks whether to save it as a permanent preference |
| *"Here's a story from last year: …"* | Uses it, and offers to add it to your Story Bank |
| *"Publish it"* / *"Publish to connections only"* | Calls `publish_post` with the exact approved text and returns the link |
| *"Delete that post"* | Calls `delete_post` after confirming which one |
### Research
> *"Research what changed in AI coding assistants this month and draft an educational post."*
Claude searches the web, lists its sources with links after the draft, attributes facts in the post, and keeps the opinion yours: it comes from your Point of View file or the chat, never from the research.
### Scheduling and automation
Use your Claude Project's **Scheduled** tasks (or scheduled routines in Claude Code, via `/schedule`) to put the work on a schedule. Some ideas:
- **Weekly ideas:** *"Every Monday at 9:00: research what's new in my topics and draft one post for my review."*
- **Story mining:** *"Every Friday: ask me one question about my week that could become a Lesson Learned post."*
- **Fully automatic:** *"Every Tuesday and Thursday at 8:30: research, draft and publish one Short Insight."*
By default, scheduled tasks stop at a draft for you to review. They publish only if the task explicitly says so, and never if the draft still has an unresolved `[confirm: …]` marker.
### Keeping your voice up to date
The reference files are the source of truth. Keep them in your fork under `claude-workspace/knowledge/`, edit them as you grow, and re-upload a file to the Project whenever you change it.
## Security
This app can post publicly as you, so it's locked down by default:
- **One owner.** The console uses Google sign-in (OpenID Connect with PKCE, state and nonce). The ID token is verified against Google's keys, and only `OWNER_EMAIL` with a verified email gets in. There's no password to guess.
- **The MCP endpoint requires auth.** claude.ai uses OAuth 2.1: discovery metadata, Dynamic Client Registration, authorization code with PKCE (S256), and rotating refresh tokens. Redirect URIs are limited to Claude's callback and localhost. Claude Code can use the static `MCP_TOKEN` instead, which is compared in constant time. Anything else gets `401` before a tool runs.
- **Secrets stay server-side.** The LinkedIn token is encrypted at rest with AES-256-GCM. OAuth codes and tokens are stored only as SHA-256 hashes. No secret reaches the browser.
- **Minimal LinkedIn scopes:** `openid profile w_member_social`.
- **Kill switches:**
- **Disconnect** deletes the LinkedIn token immediately.
- **Revoke** under *Authorized apps* cuts off a claude.ai connection.
- Rotating `MCP_TOKEN` cuts off Claude Code.
- **Every publish attempt is logged** in `publish_log`. Identical text within 24 hours is refused unless forced, so retries can't double-post.
<details>
<summary>Rotating secrets</summary>
| Secret | How to rotate | Side effect |
|---|---|---|
| `SESSION_SECRET` | Generate a new value (step 4), update it in Vercel, redeploy | Signs out all browser sessions |
| `MCP_TOKEN` | Same | Re-add the connector in Claude Code with the new token |
| `TOKEN_ENCRYPTION_KEY` | Same | The stored LinkedIn token becomes unreadable, so click Connect LinkedIn again |
| `GOOGLE_CLIENT_SECRET` | Add a new secret on the Google client, update Vercel, redeploy, then delete the old secret | None |
| `LINKEDIN_CLIENT_SECRET` | Generate a new secret on the LinkedIn Auth tab, update Vercel, redeploy | Reconnect LinkedIn |
</details>
## Configuration reference
| Variable | Required | Description |
|---|---|---|
| `DATABASE_URL` | Yes | Pooled Postgres URL (added by the Neon integration) |
| `DATABASE_URL_UNPOOLED` | For migrations | Direct Postgres URL (added by the Neon integration) |
| `SESSION_SECRET` | Yes | At least 32 characters; signs the session cookie |
| `TOKEN_ENCRYPTION_KEY` | Yes | 32 bytes, base64; encrypts the LinkedIn token |
| `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | Yes | Google OAuth client for the console login |
| `OWNER_EMAIL` | Yes | The only Google account allowed to sign in |
| `LINKEDIN_CLIENT_ID` / `LINKEDIN_CLIENT_SECRET` | Yes | LinkedIn developer app credentials |
| `MCP_TOKEN` | No | Static bearer token for Claude Code |
| `LINKEDIN_REDIRECT_URI` | No | Overrides `<origin>/api/linkedin/callback` |
| `LINKEDIN_API_VERSION` | No | LinkedIn API version (`YYYYMM`); defaults to the version in `src/lib/linkedin/posts.ts` |
## Local development
```bash
vercel env pull .env.local # Development values from step 7
npm run dev # http://localhost:3000
```
- The localhost redirect URIs from steps 5 and 6 make Google sign-in and LinkedIn connect work locally.
- Local development uses the same database as production unless you create a separate Neon branch and point `DATABASE_URL*` at it.
- After changing `src/db/schema.ts`, run `npm run db:generate` and commit the new file in `drizzle/`, then `npm run db:migrate`.
- Test the MCP server locally with the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) against `http://localhost:3000/api/mcp` and your `MCP_TOKEN`.
<details>
<summary>Project structure</summary>
```text
claude-workspace/ Claude Project instructions, reference-file templates, test prompts
drizzle/ SQL migrations
src/
app/
api/mcp/ Remote MCP server (linkedin_status, publish_post, delete_post)
api/oauth/ OAuth 2.1: client registration and token endpoint
oauth/authorize/ OAuth consent screen
.well-known/ OAuth discovery metadata
api/auth/google/ Google sign-in for the console
api/linkedin/ LinkedIn connect flow
login/, page.tsx Sign-in page and console
db/schema.ts Tables: linkedin_connection, publish_log, oauth_*
lib/
auth/ Session, Google OIDC, owner checks
data/ Data access: LinkedIn, publishing, OAuth
linkedin/ LinkedIn OAuth and Posts API clients
proxy.ts Route protection
```
</details>
## Troubleshooting
| Symptom | Fix |
|---|---|
| Google: `redirect_uri_mismatch` | The redirect URI in step 5 must be exactly `<APP_URL>/api/auth/google/callback`, with no trailing slash. |
| Google: "Access blocked" or "app not verified" | Add your account under **Audience → Test users** (step 5.3). |
| "That Google account isn't allowed" | `OWNER_EMAIL` must match the account you signed in with. Redeploy after changing it. |
| LinkedIn: "The redirect_uri does not match" | Add `<APP_URL>/api/linkedin/callback` on the LinkedIn **Auth** tab (step 6.5). |
| LinkedIn: `unauthorized_scope_error` | Both products from step 6.4 must be approved on the **Products** tab. |
| Claude: connector fails to connect | Check that `<APP_URL>/.well-known/oauth-authorization-server` returns JSON, and that you're using the production URL, not a preview URL. |
| `publish_post`: "token can't be read" | `TOKEN_ENCRYPTION_KEY` changed or differs between environments. Use the same value everywhere, then reconnect LinkedIn. |
| `publish_post`: "token has expired" | LinkedIn tokens last about 60 days. Click **Reconnect** in the console. |
| Database errors after deploying | Run `npm run db:migrate` with an up-to-date `.env.local`. |
## FAQ
**Does this cost anything?**
On free tiers (Vercel Hobby, Neon free, Google Cloud, LinkedIn), the connector costs nothing to run. Drafting uses your existing Claude plan; there are no extra AI API keys.
**Can it post to company Pages, or with images?**
Not yet. It posts text to your personal profile, which is what the self-serve "Share on LinkedIn" product allows. Pull requests are welcome.
**How often do I need to reconnect LinkedIn?**
About every 60 days. LinkedIn doesn't issue refresh tokens to self-serve apps. The console warns you 7 days before expiry.
**Can Claude post without asking me?**
Only if you set it up that way. Claude publishes when you explicitly ask, or when a scheduled task you created says to. Claude's own tool-permission prompts add another check unless you choose "always allow".
**Where does my data live?**
In your own Neon database and your own Claude Project. This repo has no telemetry and no central server.
## Contributing
Issues and pull requests are welcome. Before opening a PR, run:
```bash
npm run lint && npx tsc --noEmit && npm run build
```
## License
[MIT](LICENSE). Use it, fork it, adapt it.
---
<sub>Claude LinkedIn Studio is an independent open-source project. It isn't affiliated with, endorsed by or sponsored by LinkedIn, Microsoft, Anthropic, Google or Vercel. All trademarks belong to their owners. Follow LinkedIn's [User Agreement](https://www.linkedin.com/legal/user-agreement) and [API Terms](https://www.linkedin.com/legal/l/api-terms-of-use) when using it.</sub>
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues