Documentation
LinkedIn MCP Server
---
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:
- ❌ Create PowerPoint/PDF files (use [
chuk-mcp-pptx](https://github.com/chrishayuk/chuk-mcp-pptx) for that)
🔒 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_authdecorator) - 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
Data & Methodology
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 ChukMCPServer**: 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-mcpandlinkedin-mcp-servercommands
🔧 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:
1. Open your Claude Desktop configuration file:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
C:\Users\\AppData\Roaming\Claude\claude_desktop_config.json
(Replace `` with your actual Windows username)
2. Add the LinkedIn MCP server (no trailing slash):
{
"mcpServers": {
"linkedin": {
"url": "https://linkedin.chukai.io"
}
}
}3. Restart Claude Desktop
4. 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.ioThe 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=true3. 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 --debug4. 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
- Python 3.11 or higher
- LinkedIn OAuth credentials (create an app)
Installation Options
# Basic installation (STDIO mode only)
pip install chuk-mcp-linkedin
# Recommended: with uv (faster, more reliable)
uv pip install chuk-mcp-linkedinOptional 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 popplerFrom 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 DEBUGPython 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 instructionsPoll 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 APIPreview 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_URLenv var - Example:
https://linkedin.chukai.io/preview/abc123 - Local Development: Defaults to
http://localhost:8000/preview/abc123 - Manual Override: Can specify custom
base_urlparameter 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 --listSession Management & Artifact Storage
The server includes enterprise-grade session management and artifact storage powered by [chuk-artifacts](https://github.com/chrishayuk/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) # FalseArtifact-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="...",
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_secretfrom 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-miniSee 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 -fMakefile 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 resourcesEnvironment 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 OAuthKey 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
1. Fly.io Account - Sign up at fly.io
2. Fly CLI - Install: curl -L https://fly.io/install.sh | sh
3. LinkedIn OAuth App - Create at LinkedIn Developers
4. 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:6379Step 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_NAMEStep 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-mcpNote: 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-serverStep 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: trueTest the connection:
uvx mcp-cli --server linkedin --provider openai --model gpt-5-miniRedis Configuration
Development (Memory)
For local development, use in-memory session storage:
# .env file
SESSION_PROVIDER=memoryNo 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=redisEnvironment 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.comStep 2: Update DNS
Add DNS records (check output from previous command):
Type: CNAME
Name: linkedin.yourdomain.com
Value: your-linkedin-mcp.fly.devStep 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/callbackStep 4: Update LinkedIn App
1. Go to LinkedIn Developers
2. Select your app
3. 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_IDLogging 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=INFOSecurity 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 memoryHealth 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/mcpSecurity Best Practices
1. Never commit secrets - Use Fly secrets, not environment variables in fly.toml
2. Use HTTPS only - Set force_https = true in fly.toml
3. Rotate tokens regularly - LinkedIn tokens are auto-refreshed
4. Monitor logs - Check for failed auth attempts
5. Use custom domain - Professional appearance, easier to update
6. Enable auto-scaling - Handle traffic spikes automatically
7. 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 instancesExpected 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
- **Getting Started** - Complete beginner's guide
- **OAuth Guide** - OAuth 2.1 setup and configuration
- **API Reference** - Full API documentation
- **Themes Guide** - All themes and customization
- **Design Tokens** - Token system reference
- **Docker Guide** - Docker deployment
- **CI/CD Guide** - Continuous integration
- **Development Guide** - Contributing and development
- **Architecture** - System architecture
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 browserOutput:
🚀 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.pySee 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-installRun Tests
# Run all tests
make test
# Run with coverage
make coverage
# Run specific test
uv run pytest tests/test_composition.py -vCode Quality
# Format code
make format
# Run linter
make lint
# Type checking
make typecheck
# Security check
make security
# All quality checks
make qualityCI/CD
# Run full CI pipeline locally
make ci
# Quick CI check
make ci-quick
# Pre-commit checks
make pre-commit2025 LinkedIn Performance Data
Based on analysis of 1M+ posts across 9K company pages:
Top Performing Formats
1. Document Posts (PDF) - 45.85% engagement (HIGHEST)
- Optimal: 5-10 pages
- Format: 1920x1920 square
- Min font: 18pt for mobile
2. Poll Posts - 200%+ higher reach (MOST UNDERUSED)
- Opportunity: Least used format
- Engagement: 3x average reach
- Duration: 3-7 days optimal
3. Video Posts - 1.4x engagement (GROWING)
- Usage up 69% from 2024
- Vertical format preferred
- Keep under 3 minutes
4. Image Posts - 2x more comments than text
- Square format (1080x1080) performs best
- Infographics and data viz trending
5. Carousel Posts - Declining format
- Down 18% reach, 25% engagement vs 2024
- Keep to 5-10 slides maximum
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 configurationContributing
Contributions welcome! Please read CONTRIBUTING.md for guidelines.
Development Workflow
1. Fork the repository
2. Create feature branch (git checkout -b feature/amazing-feature)
3. Make changes and add tests
4. Run quality checks (make check)
5. Commit changes (git commit -m 'Add amazing feature')
6. Push to branch (git push origin feature/amazing-feature)
7. 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-htmlLicense
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:
- shadcn/ui - Component philosophy
- CVA - Variant system
- Model Context Protocol - MCP standard
Support
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Email: chris@chuk.ai
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.
---
Similar MCP
Based on tags & features
Trending MCP
Most active this week