Skip to main content
Glama

LinkedIn Content Creation MCP Server

by chrishayuk

LinkedIn MCP Server

Design system MCP server for creating high-performing LinkedIn content

Python 3.11+ License: MIT Code style: black MCP

FeaturesQuick StartInstallationDocumentationExamples


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:

  1. Open your Claude Desktop configuration file:

    • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json

    • Windows: C:\Users\<YourUsername>\AppData\Roaming\Claude\claude_desktop_config.json

    (Replace <YourUsername> with your actual Windows username)

  2. Add the LinkedIn MCP server (no trailing slash):

{ "mcpServers": { "linkedin": { "url": "https://linkedin.chukai.io" } } }
  1. Restart Claude Desktop

  2. 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

    • Example: https://linkedin.chukai.io/preview/abc123

  • 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

  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: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

  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_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

  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 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

  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 configuration

Contributing

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-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

-
security - not tested
A
license - permissive license
-
quality - not tested

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/chrishayuk/chuk-mcp-linkedin'

If you have feedback or need assistance with the MCP directory API, please join our Discord server