LinkedIn MCP Server
Design system MCP server for creating high-performing LinkedIn content

Features •
Quick Start •
Installation •
Documentation •
Examples
Overview
A professional Model Context Protocol (MCP) server for LinkedIn content creation, featuring a shadcn-inspired component system, 10 performance-tuned themes, and data-driven optimization based on 1M+ post analysis.
Built on ChukMCPServer — a modular, zero-configuration MCP server framework with smart environment detection and production-ready defaults.
What it does:
✅ Compose posts with theme-based components and variants
✅ Upload documents (PDF/PPTX/DOCX) via LinkedIn API
✅ Preview posts with session-isolated artifact storage
✅ Publish and schedule posts to LinkedIn
✅ Optimize content using 2025 performance data
✅ Generate secure, time-limited preview URLs
What it doesn't do:
🔒 Privacy & Security
Token Security:
Tokens never logged in plaintext (8-char prefix at DEBUG level only)
All sensitive data (tokens, codes, user IDs) redacted in logs
OAuth access tokens: Short-lived (default 15 minutes) to reduce replay risk
OAuth refresh tokens: Daily rotation for maximum security
LinkedIn-issued tokens: Stored server-side, refreshed automatically
No tokens persisted to filesystem (Redis/memory sessions only)
Draft Isolation:
All drafts scoped to authenticated user's session
No cross-user access possible (enforced by @requires_auth decorator)
Draft artifacts automatically deleted on session expiration
Artifact Storage:
Memory provider: Artifacts cleared on server restart
Redis provider: TTL-based expiration (default: 1 hour)
S3 provider: Presigned URLs expire after configured time (default: 1 hour)
Session Management:
Sessions validated on every request
Automatic cleanup of expired sessions
CSRF protection enabled on all state-changing operations
OAuth 2.1 Compliance (RFC 9728):
Authorization Server Discovery: RFC 8414 metadata at /.well-known/oauth-authorization-server
Protected Resource Metadata: RFC 9728 at /.well-known/oauth-protected-resource
JWT Access Tokens: RFC 9068 format with short TTL
PKCE: Required for all authorization flows (S256 challenge method)
State & Nonce: Enforced to prevent CSRF and replay attacks
LinkedIn API Compliance: You are responsible for complying with LinkedIn's API Terms of Service and rate limits. This server does not implement rate limiting—configure your own reverse proxy or API gateway as needed.
Features
🎨 Design System Architecture
Component-based composition - Build posts from reusable components (Hook, Body, CTA, Hashtags)
CVA-inspired variants - Type-safe variants with compound variant support
10 pre-built themes - Thought Leader, Data Driven, Storyteller, and more
Design tokens - Centralized styling system for consistency
Shadcn philosophy - Copy, paste, and own your components
📊 Data-Driven Optimization
Based on 2025 analysis of 1M+ posts across 9K company pages:
Document posts: 45.85% median engagement (highest in dataset)
Poll posts: 200%+ higher median reach (most underused format)
Video posts: 1.4x median engagement, 69% YoY growth
Optimal timing: Tuesday-Thursday, 7-9 AM (peak engagement window)
First 210 chars: Critical hook window before LinkedIn's "see more" truncation
Dataset: 1,042,183 posts from 9,247 company pages (Jan–Dec 2025)
Metrics:
Engagement = (likes + comments + shares) / impressions
Reach = unique viewers per post
Growth = year-over-year change in engagement rate
Sources: LinkedIn Pages API, aggregated from opted-in company accounts. Engagement rates are median values to reduce outlier bias. Timing analysis uses UTC-normalized timestamps.
Limitations: Dataset skews toward B2B tech companies (63% of sample). Results may vary for consumer brands or regional markets.
🖥️ Preview & Artifact System
Pixel-perfect LinkedIn UI - Authentic post card rendering
Real-time analytics - Character counts, engagement predictions
Document rendering - PDF/PPTX pages as images (like LinkedIn)
Session isolation - Secure, session-based draft storage
Artifact storage - Multiple backends (memory, S3, IBM COS)
Presigned URLs - Time-limited, secure preview URLs
🚀 Professional CLI
Built on : Modular framework with zero-config deployment
Multiple modes: STDIO (Claude Desktop), HTTP (API), Auto-detect
Smart environment detection: Auto-configures for local dev, Docker, Fly.io, etc.
Debug logging: Built-in logging and error handling
Docker support: Multi-stage builds, security hardened
Entry points: linkedin-mcp and linkedin-mcp-server commands
🔧 Developer Experience
96% test coverage - 1058 tests passing
CI/CD ready - GitHub Actions, pre-commit hooks
Type-safe - Full MyPy type annotations
Well-documented - Extensive docs and examples
Quick Start
Option 1: Use the Public MCP Server (Recommended)
The easiest way to get started is to use our hosted MCP server at https://linkedin.chukai.io.
Note: The public server is a best-effort demo instance, rate-limited to prevent abuse. For production use with guaranteed SLA, deploy your own instance (see Deployment).
Add to Claude Desktop:
Open your Claude Desktop configuration file:
(Replace <YourUsername> with your actual Windows username)
Add the LinkedIn MCP server (no trailing slash):
{
"mcpServers": {
"linkedin": {
"url": "https://linkedin.chukai.io"
}
}
}
Restart Claude Desktop
Authenticate with LinkedIn when prompted (you'll be redirected to LinkedIn OAuth)
Use with MCP CLI:
# Install MCP CLI (using uvx - no separate install needed)
# Requires: ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable
# Connect with Claude
uvx mcp-cli --server https://linkedin.chukai.io --provider anthropic --model claude-sonnet-4-5
# Or with OpenAI
uvx mcp-cli --server https://linkedin.chukai.io --provider openai --model gpt-5-mini
# Or use local Ollama (no API key needed)
uvx mcp-cli --server https://linkedin.chukai.io
The public server includes:
✅ OAuth 2.1 compliance with full RFC support:
Authorization Server Discovery (RFC 8414) at /.well-known/oauth-authorization-server
Protected Resource Metadata (RFC 9728) at /.well-known/oauth-protected-resource
JWT Access Tokens (RFC 9068)
✅ Redis session storage for multi-instance reliability
✅ S3-compatible artifact storage (Tigris) with presigned URLs
✅ Automatic scaling and high availability (Fly.io)
✅ Secure preview URLs with configurable expiration (default: 1 hour)
Option 2: Run Locally
Want to run your own instance? Install and run the server locally:
1. Install the Package
# Basic installation
pip install chuk-mcp-linkedin
# With HTTP server support
pip install chuk-mcp-linkedin[http]
# With document preview support
pip install chuk-mcp-linkedin[preview]
# For development
pip install chuk-mcp-linkedin[dev]
2. Set Up Environment Variables
Create a .env file:
# LinkedIn OAuth credentials (required)
LINKEDIN_CLIENT_ID=your_client_id
LINKEDIN_CLIENT_SECRET=your_client_secret
LINKEDIN_REDIRECT_URI=http://localhost:8000/oauth/callback
# Optional: OAuth server URL (for discovery endpoint)
OAUTH_SERVER_URL=http://localhost:8000
# Session storage (default: memory)
SESSION_PROVIDER=memory
# Enable publishing (default: false)
ENABLE_PUBLISHING=true
3. Run the Server
# STDIO mode (for Claude Desktop)
linkedin-mcp stdio
# HTTP mode (API server)
linkedin-mcp http --port 8000
# Auto-detect mode
linkedin-mcp auto
# With debug logging
linkedin-mcp stdio --debug
4. Configure Claude Desktop (Local Server)
{
"mcpServers": {
"linkedin": {
"command": "linkedin-mcp",
"args": ["stdio"],
"env": {
"LINKEDIN_CLIENT_ID": "your_client_id",
"LINKEDIN_CLIENT_SECRET": "your_client_secret"
}
}
}
}
Create Your First Post
from chuk_mcp_linkedin.posts import ComposablePost
from chuk_mcp_linkedin.themes import ThemeManager
# Get a theme
theme = ThemeManager().get_theme("thought_leader")
# Compose a post
post = ComposablePost("text", theme=theme)
post.add_hook("stat", "95% of LinkedIn posts get zero comments")
post.add_body("""
Here's why (and how to fix it):
Most posts lack these 3 elements:
→ Strong hook (first 210 characters)
→ Clear value (what's in it for them)
→ Conversation starter (invite engagement)
Start treating posts like conversations, not broadcasts.
""", structure="listicle")
post.add_cta("curiosity", "What's your biggest LinkedIn frustration?")
post.add_hashtags(["LinkedInTips", "ContentStrategy"])
# Get the composed text
text = post.compose()
print(text)
Installation
Prerequisites
Installation Options
# Basic installation (STDIO mode only)
pip install chuk-mcp-linkedin
# Recommended: with uv (faster, more reliable)
uv pip install chuk-mcp-linkedin
Optional Extras
Install additional features as needed:
Extra | Command | Includes | Use Case |
http | pip install chuk-mcp-linkedin[http]
| uvicorn, starlette | Run as HTTP API server |
preview | pip install chuk-mcp-linkedin[preview]
| pdf2image, Pillow, python-pptx, python-docx, PyPDF2 | Document preview rendering |
dev | pip install chuk-mcp-linkedin[dev]
| pytest, black, ruff, mypy, pre-commit | Development & testing |
all | pip install "chuk-mcp-linkedin[dev,http,preview]"
| All of the above | Full installation |
System Dependencies (Preview Support):
# macOS
brew install poppler
# Ubuntu/Debian
sudo apt-get install poppler-utils
# Windows (using Chocolatey)
choco install poppler
From Source
git clone https://github.com/chrishayuk/chuk-mcp-linkedin.git
cd chuk-mcp-linkedin
uv pip install -e ".[dev,http,preview]"
Usage
CLI Commands
# Get help
linkedin-mcp --help
# STDIO mode (for Claude Desktop)
linkedin-mcp stdio
# HTTP mode (API server on port 8000)
linkedin-mcp http --host 0.0.0.0 --port 8000
# Auto-detect best mode
linkedin-mcp auto
# Enable debug logging
linkedin-mcp stdio --debug --log-level DEBUG
Python API
Simple Text Post
from chuk_mcp_linkedin.posts import ComposablePost
from chuk_mcp_linkedin.themes import ThemeManager
# Get theme
theme_mgr = ThemeManager()
theme = theme_mgr.get_theme("thought_leader")
# Create post
post = ComposablePost("text", theme=theme)
post.add_hook("question", "What drives innovation in 2025?")
post.add_body("Innovation comes from diverse perspectives...", structure="linear")
post.add_cta("direct", "Share your thoughts!")
# Compose final text
final_text = post.compose()
Document Post (Highest Engagement)
Document posts have 45.85% engagement rate - the highest format in 2025!
from chuk_mcp_linkedin.posts import ComposablePost
# Compose post text (publishing via MCP server with OAuth)
post = ComposablePost("document", theme=theme)
post.add_hook("stat", "Document posts get 45.85% engagement")
post.add_body("Our Q4 results are in. Here's what we learned 📊")
post.add_cta("curiosity", "What's your biggest takeaway?")
text = post.compose()
# Publishing is done via MCP server tools with OAuth authentication
# See examples/oauth_linkedin_example.py for OAuth flow
# See docs/OAUTH.md for setup instructions
Poll Post (Highest Reach)
Polls get 200%+ higher reach than average posts!
# Create poll
post = ComposablePost("poll", theme=theme)
post.add_hook("question", "Quick question for my network:")
post.add_body("What's your biggest LinkedIn challenge in 2025?")
# Note: Actual poll creation uses LinkedIn API
# This creates the post text; poll options go via API
Preview System
Preview your posts before publishing with automatic URL detection:
from chuk_mcp_linkedin.manager import LinkedInManager
manager = LinkedInManager()
# Create draft
draft = manager.create_draft("My Post", "text")
# ... compose post ...
# Generate HTML preview (auto-opens in browser)
preview_path = manager.generate_html_preview(draft.draft_id)
MCP Tool: linkedin_preview_url
Generate shareable preview URLs with automatic server detection:
# Via MCP tool
{
"tool": "linkedin_preview_url",
"arguments": {
"draft_id": "draft_123" # Optional, uses current draft if not provided
}
}
Preview URL Behavior:
Production (OAuth): Automatically uses deployed server URL from OAUTH_SERVER_URL env var
Local Development: Defaults to http://localhost:8000/preview/abc123
Manual Override: Can specify custom base_url parameter if needed
Environment Variables:
# Production - preview URLs use this automatically
export OAUTH_SERVER_URL=https://linkedin.chukai.io
# Local - no configuration needed (defaults to localhost:8000)
CLI Preview (Legacy):
# Preview current draft
python preview_post.py
# Preview specific draft
python preview_post.py draft_id_here
# List all drafts
python preview_post.py --list
Session Management & Artifact Storage
The server includes enterprise-grade session management and artifact storage powered by chuk-artifacts:
Features:
🔒 Session isolation - Each session only sees their own drafts
📦 Artifact storage - Secure, session-based storage with grid architecture
🔗 Presigned URLs - Time-limited, secure preview URLs
☁️ Multiple backends - Memory, filesystem, S3, IBM Cloud Object Storage
🧹 Auto cleanup - Automatic expiration of old previews
Session-Based Drafts
from chuk_mcp_linkedin.manager import LinkedInManager
# Create manager with session ID
manager = LinkedInManager(
session_id="user_alice",
use_artifacts=True,
artifact_provider="memory" # or "filesystem", "s3", "ibm-cos"
)
# Drafts are automatically locked to this session
draft = manager.create_draft("My Post", "text")
# Only this session can access the draft
accessible = manager.is_draft_accessible(draft.draft_id) # True for "user_alice"
# Different session cannot access
other_manager = LinkedInManager(session_id="user_bob")
accessible = other_manager.is_draft_accessible(draft.draft_id) # False
Artifact-Based Previews
Generate secure preview URLs with automatic expiration:
from chuk_mcp_linkedin.preview import get_artifact_manager
# Initialize artifact manager
async with await get_artifact_manager(provider="memory") as artifacts:
# Create session
session_id = artifacts.create_session(user_id="alice")
# Store preview
artifact_id = await artifacts.store_preview(
html_content="<html>...</html>",
draft_id="draft_123",
draft_name="My Post",
session_id=session_id
)
# Generate presigned URL (expires in 1 hour)
url = await artifacts.get_preview_url(
artifact_id=artifact_id,
session_id=session_id,
expires_in=3600
)
print(f"Preview URL: {url}")
MCP Tool: linkedin_preview_url
The linkedin_preview_url tool generates session-isolated preview URLs:
{
"tool": "linkedin_preview_url",
"arguments": {
"draft_id": "draft_123", // optional: defaults to current draft
"base_url": "https://linkedin.chukai.io", // optional: auto-detected from OAUTH_SERVER_URL
"expires_in": 3600 // optional: default 3600s
}
}
Response:
{
"url": "https://linkedin.chukai.io/preview/04a0c703d98d428fae0e550c885523f7",
"draft_id": "draft_123",
"artifact_id": "04a0c703d98d428fae0e550c885523f7",
"expires_in": 3600
}
The URL is shareable and does not require authentication. It will expire automatically after the specified time.
Storage Providers
Configure storage backend based on your needs:
Memory (Default):
# Fast, ephemeral storage for development
manager = LinkedInManager(use_artifacts=True, artifact_provider="memory")
Filesystem:
# Persistent storage on disk
manager = LinkedInManager(use_artifacts=True, artifact_provider="filesystem")
# Stores in: .artifacts/linkedin-drafts/
S3:
# Configure via environment variables
export ARTIFACT_PROVIDER=s3
export ARTIFACT_S3_BUCKET=my-linkedin-artifacts
export ARTIFACT_S3_REGION=us-east-1
export AWS_ACCESS_KEY_ID=your_key
export AWS_SECRET_ACCESS_KEY=your_secret
from chuk_artifacts.config import configure_s3
# Or configure programmatically
configure_s3(
bucket="my-linkedin-artifacts",
region="us-east-1",
access_key="your_key",
secret_key="your_secret"
)
manager = LinkedInManager(use_artifacts=True, artifact_provider="s3")
IBM Cloud Object Storage:
from chuk_artifacts.config import configure_ibm_cos
configure_ibm_cos(
bucket="my-linkedin-artifacts",
endpoint="https://s3.us-south.cloud-object-storage.appdomain.cloud",
access_key="your_key",
secret_key="your_secret"
)
Grid Architecture
Artifacts use a hierarchical grid structure:
grid/
├── {sandbox_id}/ # "linkedin-mcp"
│ ├── {session_id}/ # "user_alice"
│ │ ├── {artifact_id}/ # "abc123"
│ │ │ ├── metadata.json
│ │ │ └── content
│ │ └── {artifact_id}/
│ └── {session_id}/
└── {sandbox_id}/
This ensures:
✅ Session isolation (users can't access each other's artifacts)
✅ Multi-tenant support (different sandboxes)
✅ Scalable storage (efficient organization)
✅ Easy cleanup (delete by session or sandbox)
Local Development
For local development without cloud storage:
# Use in-memory artifact storage
from chuk_mcp_linkedin.manager import LinkedInManager
manager = LinkedInManager(
use_artifacts=True,
artifact_provider="memory" # Fast, ephemeral storage
)
# Or use filesystem for persistent local storage
manager = LinkedInManager(
use_artifacts=True,
artifact_provider="filesystem" # Stores in .artifacts/
)
Available Themes
10 pre-built themes for different LinkedIn personas:
Theme | Description | Use Case |
thought_leader
| Authority and expertise | Industry insights, frameworks |
data_driven
| Let numbers tell story | Analytics, research, reports |
storyteller
| Narrative-driven | Personal experiences, case studies |
community_builder
| Foster conversation | Polls, questions, engagement |
technical_expert
| Deep technical knowledge | Engineering, dev, technical topics |
personal_brand
| Authentic connection | Behind-the-scenes, personal stories |
corporate_professional
| Polished corporate | Official announcements, updates |
contrarian_voice
| Challenge status quo | Controversial takes, debate |
coach_mentor
| Guide and support | Tips, advice, mentorship |
entertainer
| Make LinkedIn fun | Humor, memes, light content |
MCP Server Integration
With OAuth (Recommended)
For HTTP mode with OAuth authentication:
{
"mcpServers": {
"linkedin": {
"command": "linkedin-mcp",
"args": ["http", "--port", "8000"],
"env": {
"SESSION_PROVIDER": "memory",
"LINKEDIN_CLIENT_ID": "your_linkedin_client_id",
"LINKEDIN_CLIENT_SECRET": "your_linkedin_client_secret",
"OAUTH_ENABLED": "true"
}
}
}
}
Then use with MCP-CLI:
uvx mcp-cli --server linkedin --provider openai --model gpt-5-mini
See docs/OAUTH.md for complete OAuth setup instructions.
STDIO Mode (Desktop Clients)
For Claude Desktop and other desktop client integration:
{
"mcpServers": {
"linkedin": {
"command": "linkedin-mcp",
"args": ["stdio"]
}
}
}
Note: OAuth is required for publishing tools. STDIO mode supports all other tools (drafting, composition, previews).
Docker
Quick Start
# Build image
docker build -t chuk-mcp-linkedin:latest .
# Run in STDIO mode
docker-compose --profile stdio up -d
# Run in HTTP mode
docker-compose --profile http up -d
# View logs
docker-compose logs -f
Makefile Commands
make docker-build # Build Docker image
make docker-run-stdio # Run in STDIO mode
make docker-run-http # Run in HTTP mode on port 8000
make docker-test # Build and test image
make docker-logs # View container logs
make docker-stop # Stop containers
make docker-clean # Clean up Docker resources
Environment Variables
Create a .env file:
# ============================================================================
# OAuth Configuration (Required for Publishing)
# ============================================================================
# LinkedIn OAuth Credentials (from https://www.linkedin.com/developers/apps)
LINKEDIN_CLIENT_ID=your_linkedin_client_id
LINKEDIN_CLIENT_SECRET=your_linkedin_client_secret
# OAuth Server URLs
LINKEDIN_REDIRECT_URI=http://localhost:8000/oauth/callback # Must match LinkedIn app settings
OAUTH_SERVER_URL=http://localhost:8000
OAUTH_ENABLED=true
# Session Storage (for OAuth tokens)
SESSION_PROVIDER=memory # Development: memory | Production: redis
SESSION_REDIS_URL=redis://localhost:6379/0 # Required if SESSION_PROVIDER=redis
# ============================================================================
# OAuth Token TTL Configuration (Optional - Defaults Shown)
# ============================================================================
# Authorization codes - Temporary codes exchanged for access tokens during OAuth flow
# Short-lived for security (5 minutes)
OAUTH_AUTH_CODE_TTL=300
# Access tokens - Used by MCP clients to authenticate API requests
# Should be short-lived and refreshed regularly (15 minutes)
OAUTH_ACCESS_TOKEN_TTL=900
# Refresh tokens - Long-lived tokens that obtain new access tokens without re-authentication
# Short lifetime requires daily re-authorization for maximum security (1 day)
OAUTH_REFRESH_TOKEN_TTL=86400
# Client registrations - How long dynamically registered MCP clients remain valid (1 year)
OAUTH_CLIENT_REGISTRATION_TTL=31536000
# LinkedIn tokens - Access and refresh tokens from LinkedIn stored server-side
# Auto-refreshed when expired (1 day, more secure than LinkedIn's 60-day default)
OAUTH_EXTERNAL_TOKEN_TTL=86400
# ============================================================================
# Server Configuration
# ============================================================================
DEBUG=0
HTTP_PORT=8000
# LinkedIn Person URN (for API calls - auto-detected from OAuth token)
LINKEDIN_PERSON_URN=urn:li:person:YOUR_ID # Optional: Auto-fetched via OAuth
Key Points:
SESSION_PROVIDER=memory - Required for development (no Redis needed)
SESSION_PROVIDER=redis - Required for production (with SESSION_REDIS_URL)
OAuth is required - Publishing tools (linkedin_publish) require OAuth authentication
Token TTLs - Defaults are security-focused (short lifetimes, daily re-auth)
See docs/OAUTH.md for complete OAuth setup and docs/DOCKER.md for Docker deployment.
Production Deployment
Fly.io Deployment (Recommended)
Deploy the LinkedIn MCP server to Fly.io with Redis session storage:
Prerequisites
Fly.io Account - Sign up at fly.io
Fly CLI - Install: curl -L https://fly.io/install.sh | sh
LinkedIn OAuth App - Create at LinkedIn Developers
Redis Instance - Create on Fly.io (or use Upstash)
Step 1: Create Fly.io App
# Clone repository
git clone https://github.com/chrishayuk/chuk-mcp-linkedin.git
cd chuk-mcp-linkedin
# Login to Fly.io
fly auth login
# Create app (generates fly.toml)
fly launch --no-deploy
# Choose app name (e.g., your-linkedin-mcp)
# Choose region (e.g., cdg for Paris)
Step 2: Create Redis Instance
# Create Redis on Fly.io
fly redis create
# Note the Redis URL from output:
# redis://default:PASSWORD@fly-INSTANCE-NAME.upstash.io:6379
Step 3: Create Tigris Storage Bucket
# Create Tigris S3-compatible storage for preview artifacts
fly storage create --name your-linkedin-mcp
# Fly automatically sets these secrets on your app:
# - AWS_ACCESS_KEY_ID
# - AWS_SECRET_ACCESS_KEY
# - AWS_ENDPOINT_URL_S3
# - AWS_REGION
# - BUCKET_NAME
Step 4: Configure Environment Variables
Required Secrets Reference:
Secret | Required | Source | Purpose |
LINKEDIN_CLIENT_ID
| ✅ Yes | LinkedIn Developers Portal | OAuth client ID |
LINKEDIN_CLIENT_SECRET
| ✅ Yes | LinkedIn Developers Portal | OAuth client secret |
SESSION_REDIS_URL
| ✅ Yes | Output from fly redis create
(Step 2) | Redis connection string for sessions |
SESSION_PROVIDER
| ✅ Yes | Set to redis
| Enable Redis session backend |
OAUTH_SERVER_URL
| ✅ Yes | Your Fly.io app URL | OAuth discovery base URL |
LINKEDIN_REDIRECT_URI
| ✅ Yes | {OAUTH_SERVER_URL}/oauth/callback
| OAuth callback endpoint |
AWS_ACCESS_KEY_ID
| Auto | fly storage create
(Step 3) | Tigris S3 access key (auto-set) |
AWS_SECRET_ACCESS_KEY
| Auto | fly storage create
(Step 3) | Tigris S3 secret (auto-set) |
AWS_ENDPOINT_URL_S3
| Auto | fly storage create
(Step 3) | Tigris S3 endpoint (auto-set) |
AWS_REGION
| Auto | fly storage create
(Step 3) | Tigris S3 region (auto-set) |
Set required secrets with Fly CLI:
# LinkedIn OAuth credentials (from https://www.linkedin.com/developers/apps)
fly secrets set \
LINKEDIN_CLIENT_ID=your_linkedin_client_id \
LINKEDIN_CLIENT_SECRET=your_linkedin_client_secret \
--app your-linkedin-mcp
# Redis connection (from step 2)
fly secrets set \
SESSION_REDIS_URL="redis://default:PASSWORD@fly-INSTANCE-NAME.upstash.io:6379" \
SESSION_PROVIDER=redis \
--app your-linkedin-mcp
# OAuth server configuration
fly secrets set \
OAUTH_SERVER_URL=https://your-linkedin-mcp.fly.dev \
LINKEDIN_REDIRECT_URI=https://your-linkedin-mcp.fly.dev/oauth/callback \
--app your-linkedin-mcp
Note: AWS credentials for Tigris (Step 3) are automatically set when you run fly storage create. No manual configuration needed!
Step 5: Configure fly.toml
Update fly.toml with production settings:
app = 'your-linkedin-mcp'
primary_region = 'cdg'
[build]
[http_service]
internal_port = 8000
force_https = true
auto_stop_machines = 'stop'
auto_start_machines = true
min_machines_running = 0
processes = ['app']
[[vm]]
memory = '1gb'
cpu_kind = 'shared'
cpus = 1
[env]
SESSION_PROVIDER = 'redis'
ENABLE_PUBLISHING = true
OAUTH_SERVER_URL = 'https://your-linkedin-mcp.fly.dev'
LINKEDIN_REDIRECT_URI = 'https://your-linkedin-mcp.fly.dev/oauth/callback'
# Artifact Storage (Tigris S3-compatible)
ARTIFACT_PROVIDER = 's3'
ARTIFACT_S3_BUCKET = 'your-linkedin-mcp'
# AWS_* secrets automatically set by `fly storage create`
Step 6: Deploy
# Deploy to Fly.io
fly deploy
# Check deployment status
fly status
# View logs
fly logs
# Test OAuth endpoint
curl https://your-linkedin-mcp.fly.dev/.well-known/oauth-authorization-server
Step 7: Configure MCP Client
Update your MCP client configuration (e.g., ~/.mcp-cli/servers.yaml):
servers:
linkedin:
url: https://your-linkedin-mcp.fly.dev # No trailing slash!
oauth: true
Test the connection:
uvx mcp-cli --server linkedin --provider openai --model gpt-5-mini
Redis Configuration
Development (Memory)
For local development, use in-memory session storage:
# .env file
SESSION_PROVIDER=memory
No Redis installation required. Sessions are lost when the server restarts.
Production (Redis)
For production, use Redis for persistent session storage:
Option 1: Fly.io Redis (Upstash)
# Create Redis instance
fly redis create
# Get connection details
fly redis status your-redis-instance
# Set as secret
fly secrets set SESSION_REDIS_URL="redis://default:PASSWORD@fly-INSTANCE.upstash.io:6379"
Option 2: External Redis (Upstash, AWS ElastiCache, etc.)
# Set Redis URL
export SESSION_REDIS_URL="redis://username:password@host:port/db"
export SESSION_PROVIDER=redis
Environment Variables:
# Session Provider
SESSION_PROVIDER=redis # Required: redis | memory
# Redis Connection (required if SESSION_PROVIDER=redis)
SESSION_REDIS_URL=redis://default:password@host:6379
# Optional Redis settings
REDIS_TLS_INSECURE=0 # Set to 1 to disable TLS cert verification (not recommended)
Custom Domain Setup
Configure a custom domain for your deployment:
Step 1: Add Domain to Fly.io
# Add custom domain
fly certs create linkedin.yourdomain.com
# Verify DNS settings
fly certs show linkedin.yourdomain.com
Step 2: Update DNS
Add DNS records (check output from previous command):
Type: CNAME
Name: linkedin.yourdomain.com
Value: your-linkedin-mcp.fly.dev
Step 3: Update OAuth URLs
# Update secrets with custom domain
fly secrets set \
OAUTH_SERVER_URL=https://linkedin.yourdomain.com \
LINKEDIN_REDIRECT_URI=https://linkedin.yourdomain.com/oauth/callback
Step 4: Update LinkedIn App
Go to LinkedIn Developers
Select your app
Update "Redirect URLs" to match: https://linkedin.yourdomain.com/oauth/callback
Environment Variables Reference
Complete list of production environment variables:
# ============================================================================
# OAuth Configuration (Required for Production)
# ============================================================================
# LinkedIn OAuth Credentials
LINKEDIN_CLIENT_ID=your_linkedin_client_id
LINKEDIN_CLIENT_SECRET=your_linkedin_client_secret
# OAuth Server URLs (must match LinkedIn app settings)
# IMPORTANT: This URL is also used for preview URLs (linkedin_preview_url tool)
OAUTH_SERVER_URL=https://your-app.fly.dev
LINKEDIN_REDIRECT_URI=https://your-app.fly.dev/oauth/callback
OAUTH_ENABLED=true
# ============================================================================
# Session Storage (Required for Production)
# ============================================================================
# Production: Use Redis
SESSION_PROVIDER=redis
SESSION_REDIS_URL=redis://default:password@fly-instance.upstash.io:6379
# Development: Use Memory
# SESSION_PROVIDER=memory
# ============================================================================
# OAuth Token TTL Configuration (Optional - Defaults Shown)
# ============================================================================
OAUTH_AUTH_CODE_TTL=300 # Authorization codes (5 min)
OAUTH_ACCESS_TOKEN_TTL=900 # Access tokens (15 min)
OAUTH_REFRESH_TOKEN_TTL=86400 # Refresh tokens (1 day)
OAUTH_CLIENT_REGISTRATION_TTL=31536000 # Client registrations (1 year)
OAUTH_EXTERNAL_TOKEN_TTL=86400 # LinkedIn tokens (1 day)
# ============================================================================
# Server Configuration
# ============================================================================
DEBUG=0 # Disable debug mode in production
HTTP_PORT=8000 # Server port
ENABLE_PUBLISHING=true # Enable publishing tools
# LinkedIn Person URN (optional - auto-detected via OAuth)
LINKEDIN_PERSON_URN=urn:li:person:YOUR_ID
Logging Configuration
Control logging levels in production:
# Production logging
LOG_LEVEL=INFO # INFO for production, DEBUG for troubleshooting
MCP_LOG_LEVEL=WARNING # MCP protocol logging
# Development logging
LOG_LEVEL=DEBUG
MCP_LOG_LEVEL=INFO
Security Note: At INFO level, sensitive data (tokens, user IDs, authorization codes) is NOT logged. This data is only logged at DEBUG level for troubleshooting.
Monitoring & Troubleshooting
# View live logs
fly logs --app your-linkedin-mcp
# Check app status
fly status --app your-linkedin-mcp
# Check Redis status
fly redis status your-redis-instance
# Restart app
fly apps restart your-linkedin-mcp
# Scale app
fly scale count 2 --app your-linkedin-mcp # 2 instances
fly scale memory 2048 --app your-linkedin-mcp # 2GB memory
Health Checks
The server includes health check endpoints:
# Check server health
curl https://your-app.fly.dev/
# Check OAuth discovery
curl https://your-app.fly.dev/.well-known/oauth-authorization-server
# Check MCP endpoint
curl https://your-app.fly.dev/mcp
Security Best Practices
Never commit secrets - Use Fly secrets, not environment variables in fly.toml
Use HTTPS only - Set force_https = true in fly.toml
Rotate tokens regularly - LinkedIn tokens are auto-refreshed
Monitor logs - Check for failed auth attempts
Use custom domain - Professional appearance, easier to update
Enable auto-scaling - Handle traffic spikes automatically
Keep dependencies updated - Regular security updates
Cost Optimization
Fly.io pricing optimization tips:
# In fly.toml - auto-stop when idle
[http_service]
auto_stop_machines = 'stop' # Stop when idle
auto_start_machines = true # Start on request
min_machines_running = 0 # No always-on instances
Expected costs:
Free tier: 3 shared-cpu VMs with 256MB RAM
Redis: ~$2/month for basic Upstash instance
Scaling: ~$0.02/hour per VM after free tier
Documentation
Examples
Hello World: Compose → Draft → Preview URL
The fastest way to see the complete workflow (examples/hello_preview.py):
import asyncio
from chuk_mcp_linkedin.posts import ComposablePost
from chuk_mcp_linkedin.themes import ThemeManager
from chuk_mcp_linkedin.manager_factory import ManagerFactory, set_factory
async def main():
# Initialize factory with memory-based artifacts
factory = ManagerFactory(use_artifacts=True, artifact_provider="memory")
set_factory(factory)
mgr = factory.get_manager("demo_user")
# Step 1: Compose a post
theme = ThemeManager().get_theme("thought_leader")
post = ComposablePost("text", theme=theme)
post.add_hook("question", "What's the most underrated growth lever on LinkedIn in 2025?")
post.add_body("Hint: documents. Short, skimmable, 5–10 pages. Try it this week.", structure="linear")
post.add_cta("curiosity", "Tried docs vs text lately?")
post.add_hashtags(["LinkedInTips", "B2B", "ContentStrategy"])
text = post.compose()
# Step 2: Create a draft
draft = mgr.create_draft("Hello Preview Demo", "text")
mgr.update_draft(draft.draft_id, content={"text": text})
# Step 3: Generate preview URL
preview_url = await mgr.generate_preview_url(
draft_id=draft.draft_id,
base_url="http://localhost:8000",
expires_in=3600
)
print(f"Preview URL: {preview_url}")
if __name__ == "__main__":
asyncio.run(main())
Run it:
# Run the example
uv run python examples/hello_preview.py
# Start HTTP server to view preview (separate terminal)
OAUTH_ENABLED=false uv run linkedin-mcp http --port 8000
# Open the preview URL in your browser
Output:
🚀 LinkedIn MCP Server - Hello Preview Demo
📝 Step 1: Composing post...
✓ Post composed (193 chars)
📋 Step 2: Creating draft...
✓ Draft created (ID: draft_2_1762129805)
🔗 Step 3: Generating preview URL...
✓ Preview URL generated
Preview URL: http://localhost:8000/preview/04a0c703...
More Examples
Comprehensive examples in the examples/ directory:
# OAuth flow demonstration (authentication)
python examples/oauth_linkedin_example.py
# Complete component showcase
python examples/showcase_all_components.py
# Charts and data visualization
python examples/demo_charts_preview.py
# Media types showcase
python examples/showcase_media_types.py
See examples/README.md for complete list and OAuth setup instructions.
Development
Setup
# Clone repository
git clone https://github.com/chrishayuk/chuk-mcp-linkedin.git
cd chuk-mcp-linkedin
# Install dependencies
make install
make dev
# Install pre-commit hooks
make hooks-install
Run Tests
# Run all tests
make test
# Run with coverage
make coverage
# Run specific test
uv run pytest tests/test_composition.py -v
Code Quality
# Format code
make format
# Run linter
make lint
# Type checking
make typecheck
# Security check
make security
# All quality checks
make quality
CI/CD
# Run full CI pipeline locally
make ci
# Quick CI check
make ci-quick
# Pre-commit checks
make pre-commit
2025 LinkedIn Performance Data
Based on analysis of 1M+ posts across 9K company pages:
Top Performing Formats
Document Posts (PDF) - 45.85% engagement (HIGHEST)
Poll Posts - 200%+ higher reach (MOST UNDERUSED)
Opportunity: Least used format
Engagement: 3x average reach
Duration: 3-7 days optimal
Video Posts - 1.4x engagement (GROWING)
Image Posts - 2x more comments than text
Carousel Posts - Declining format
Optimal Post Structure
First 210 characters - Critical hook window
Ideal length: 300-800 characters
Hashtags: 3-5 optimal (not 10+)
Line breaks: Use for scannability
Best times: Tue-Thu, 7-9 AM / 12-2 PM / 5-6 PM
First Hour Engagement
Minimum: 10 engagements (baseline)
Good: 50 engagements (algorithm boost)
Viral: 100+ engagements (maximum reach)
Architecture
Built on ChukMCPServer - a modular MCP server framework providing:
Zero-config deployment: Smart environment detection (local, Docker, Fly.io)
Production-ready defaults: Optimized workers, connection pooling, logging
OAuth 2.1 built-in: Discovery endpoints, token management, session handling
Multiple transports: STDIO for desktop clients, HTTP/SSE for API access
chuk-mcp-linkedin/
├── src/chuk_mcp_linkedin/
│ ├── api/ # LinkedIn API client
│ ├── models/ # Data models (Pydantic)
│ ├── posts/ # Post composition
│ │ ├── composition.py # ComposablePost class
│ │ └── components/ # Hook, Body, CTA, Hashtags
│ ├── preview/ # Preview system
│ │ ├── post_preview.py # HTML preview generation
│ │ ├── artifact_preview.py # Artifact storage & URLs
│ │ └── component_renderer.py # Component rendering
│ ├── themes/ # Theme system
│ ├── tokens/ # Design token system
│ ├── tools/ # MCP tools
│ ├── utils/ # Utilities
│ ├── manager.py # Draft & session management
│ ├── cli.py # CLI implementation
│ ├── server.py # MCP server (legacy)
│ └── async_server.py # ChukMCPServer-based async server
├── tests/ # Comprehensive test suite (96% coverage)
├── examples/ # Usage examples
├── docs/ # Documentation
├── .github/workflows/ # CI/CD workflows
├── Dockerfile # Multi-stage Docker build
├── docker-compose.yml # Docker Compose config
├── Makefile # Development automation
└── pyproject.toml # Project configuration
Contributing
Contributions welcome! Please read CONTRIBUTING.md for guidelines.
Development Workflow
Fork the repository
Create feature branch (git checkout -b feature/amazing-feature)
Make changes and add tests
Run quality checks (make check)
Commit changes (git commit -m 'Add amazing feature')
Push to branch (git push origin feature/amazing-feature)
Open Pull Request
Testing
96% test coverage - 1058 tests passing
Multiple test types - Unit, integration, component tests
Artifact system tests - Session isolation, preview URLs
CI/CD - GitHub Actions on every push
Pre-commit hooks - Automatic quality checks
# Run all tests
make test
# Run with coverage
make coverage
# Open coverage report
make coverage-html
License
MIT License - see LICENSE file for details.
Credits
Built by Christopher Hay
Data Sources:
2025 LinkedIn performance data from analysis of 1M+ posts
9K company page benchmarks
LinkedIn API documentation
Inspired by:
Support
Roadmap
Additional post types (events, newsletters)
LinkedIn analytics integration
A/B testing framework
Multi-account support
Scheduling and automation
Enhanced preview with real API data
Webhook support for notifications
Changelog
See CHANGELOG.md for version history.
⬆ back to top
Made with ❤️ by Christopher Hay