Skip to main content
Glama

LinkedIn Content Creation MCP Server

by chrishayuk
composition.pyβ€’11.5 kB
# src/chuk_mcp_linkedin/posts/composition.py """ Composition system for building LinkedIn posts from components. Shadcn-style atomic composition with type-safe components. """ from typing import Any, Dict, List, Optional from ..tokens.text_tokens import TextTokens from .components import ( BarChart, BeforeAfter, BigStat, Body, CallToAction, Checklist, ComparisonChart, FeatureList, Hashtags, Hook, KeyTakeaway, MetricsChart, NumberedList, PollPreview, PostComponent, ProCon, ProgressChart, Quote, RankingChart, Separator, StatsGrid, Timeline, TipBox, ) class ComposablePost: """Shadcn-style composition for LinkedIn posts""" def __init__( self, post_type: str, theme: Optional[Any] = None, variant_config: Optional[Dict[str, Any]] = None, ): self.post_type = post_type self.theme = theme self.variant_config = variant_config or {} self.components: List[PostComponent] = [] self.metadata: Dict[str, Any] = {} # Content components def add_hook(self, hook_type: str, content: str) -> "ComposablePost": """Add opening hook""" self.components.append(Hook(hook_type, content, self.theme)) return self def add_body(self, content: str, structure: Optional[str] = None) -> "ComposablePost": """Add main content body""" structure = structure or self.variant_config.get("structure", "linear") self.components.append(Body(content, structure, self.theme)) return self def add_cta(self, cta_type: str, text: str) -> "ComposablePost": """Add call-to-action""" self.components.append(CallToAction(cta_type, text, self.theme)) return self def add_hashtags(self, tags: List[str], placement: str = "end") -> "ComposablePost": """Add hashtags""" self.components.append(Hashtags(tags, placement, theme=self.theme)) return self # Data viz components def add_bar_chart( self, data: Dict[str, int], title: Optional[str] = None, unit: str = "" ) -> "ComposablePost": """Add horizontal bar chart with colored emoji squares""" self.components.append(BarChart(data, title, unit, self.theme)) return self def add_metrics_chart( self, data: Dict[str, str], title: Optional[str] = None ) -> "ComposablePost": """Add key metrics chart with indicators""" self.components.append(MetricsChart(data, title, self.theme)) return self def add_comparison_chart( self, data: Dict[str, Any], title: Optional[str] = None ) -> "ComposablePost": """Add side-by-side comparison chart""" self.components.append(ComparisonChart(data, title, self.theme)) return self def add_progress_chart( self, data: Dict[str, int], title: Optional[str] = None ) -> "ComposablePost": """Add progress bars chart""" self.components.append(ProgressChart(data, title, self.theme)) return self def add_ranking_chart( self, data: Dict[str, str], title: Optional[str] = None, show_medals: bool = True ) -> "ComposablePost": """Add ranking/leaderboard chart with medals""" self.components.append(RankingChart(data, title, show_medals, self.theme)) return self # Feature components def add_quote(self, text: str, author: str, source: Optional[str] = None) -> "ComposablePost": """Add quote/testimonial""" self.components.append(Quote(text, author, source, self.theme)) return self def add_big_stat( self, number: str, label: str, context: Optional[str] = None ) -> "ComposablePost": """Add big statistic display""" self.components.append(BigStat(number, label, context, self.theme)) return self def add_timeline( self, steps: Dict[str, str], title: Optional[str] = None, style: str = "arrow" ) -> "ComposablePost": """Add timeline/step display""" self.components.append(Timeline(steps, title, style, self.theme)) return self def add_key_takeaway( self, message: str, title: str = "KEY TAKEAWAY", style: str = "box" ) -> "ComposablePost": """Add key takeaway/insight box""" self.components.append(KeyTakeaway(message, title, style, self.theme)) return self def add_pro_con( self, pros: List[str], cons: List[str], title: Optional[str] = None ) -> "ComposablePost": """Add pros & cons comparison""" self.components.append(ProCon(pros, cons, title, self.theme)) return self def add_checklist( self, items: List[Dict[str, Any]], title: Optional[str] = None, show_progress: bool = False ) -> "ComposablePost": """Add checklist with checkmarks""" self.components.append(Checklist(items, title, show_progress, self.theme)) return self def add_before_after( self, before: List[str], after: List[str], title: Optional[str] = None, labels: Optional[Dict[str, str]] = None, ) -> "ComposablePost": """Add before/after transformation comparison""" self.components.append(BeforeAfter(before, after, title, labels, self.theme)) return self def add_tip_box( self, message: str, title: Optional[str] = None, style: str = "info" ) -> "ComposablePost": """Add highlighted tip/note box""" self.components.append(TipBox(message, title, style, self.theme)) return self def add_stats_grid( self, stats: Dict[str, str], title: Optional[str] = None, columns: int = 2 ) -> "ComposablePost": """Add multi-stat grid display""" self.components.append(StatsGrid(stats, title, columns, self.theme)) return self def add_poll_preview(self, question: str, options: List[str]) -> "ComposablePost": """Add poll preview for engagement""" self.components.append(PollPreview(question, options, self.theme)) return self def add_feature_list( self, features: List[Dict[str, str]], title: Optional[str] = None ) -> "ComposablePost": """Add feature list with icons and descriptions""" self.components.append(FeatureList(features, title, self.theme)) return self def add_numbered_list( self, items: List[str], title: Optional[str] = None, style: str = "numbers", start: int = 1 ) -> "ComposablePost": """Add enhanced numbered list""" self.components.append(NumberedList(items, title, style, start, self.theme)) return self # Layout components def add_separator(self, style: str = "line") -> "ComposablePost": """Add visual separator""" self.components.append(Separator(style)) return self # Composition methods def compose(self) -> str: """Compose final post text""" sections = [] for component in self.components: if component.validate(): rendered = component.render(self.theme) sections.append(rendered) final_text = "\n\n".join(sections) # Validate total length if len(final_text) > TextTokens.MAX_LENGTH: raise ValueError( f"Post exceeds {TextTokens.MAX_LENGTH} character limit: {len(final_text)} chars" ) return final_text def get_preview(self, chars: int = 210) -> str: """Get truncated preview (what users see before 'see more')""" full_text = self.compose() if len(full_text) <= chars: return full_text return full_text[:chars] + "..." def optimize_for_engagement(self) -> "ComposablePost": """Apply engagement optimizations""" # Ensure hook exists has_hook = any(isinstance(c, Hook) for c in self.components) if not has_hook and self.theme: hook_style = getattr(self.theme, "hook_style", "question") self.components.insert(0, Hook(hook_style, "", self.theme)) # Ensure CTA exists has_cta = any(isinstance(c, CallToAction) for c in self.components) if not has_cta and self.theme: cta_style = getattr(self.theme, "cta_style", "curiosity") self.components.append(CallToAction(cta_style, "What's your take?", self.theme)) return self def to_dict(self) -> Dict[str, Any]: """Export as dictionary""" return { "post_type": self.post_type, "theme": self.theme.name if self.theme else None, "components": [ {"type": type(c).__name__, "content": c.render(self.theme)} for c in self.components ], "final_text": self.compose(), "character_count": len(self.compose()), "preview": self.get_preview(), } class PostBuilder: """Fluent builder for common post patterns""" @staticmethod def thought_leadership_post( hook_stat: str, framework_name: str, framework_parts: List[str], conclusion: str, theme: Any ) -> ComposablePost: """Pre-built thought leadership pattern""" post = ComposablePost("text", theme=theme) post.add_hook("stat", hook_stat) post.add_body(f"Here's the {framework_name}:", structure="linear") framework_text = "||".join(framework_parts) post.add_body(framework_text, structure="framework") post.add_separator("line") post.add_body(conclusion, structure="linear") post.add_cta("curiosity", "Which resonates most with you?") post.add_hashtags([framework_name.replace(" ", ""), "Leadership", "Strategy"]) return post @staticmethod def story_post( hook: str, problem: str, journey: str, solution: str, lesson: str, theme: Any ) -> ComposablePost: """Pre-built story arc pattern""" post = ComposablePost("text", theme=theme) post.add_hook("story", hook) story = f"{problem}\n\n{journey}\n\n{solution}" post.add_body(story, structure="story_arc") post.add_separator("dots") post.add_body(f"The lesson: {lesson}", structure="linear") post.add_cta("soft", "Have you experienced something similar?") return post @staticmethod def listicle_post(hook: str, items: List[str], conclusion: str, theme: Any) -> ComposablePost: """Pre-built listicle pattern""" post = ComposablePost("text", theme=theme) post.add_hook("list", hook) list_content = "\n".join(items) post.add_body(list_content, structure="listicle") post.add_separator("wave") post.add_body(conclusion, structure="linear") post.add_cta("action", "Save this for later") return post @staticmethod def comparison_post( hook: str, option_a: str, option_b: str, recommendation: str, theme: Any ) -> ComposablePost: """Pre-built comparison pattern""" post = ComposablePost("text", theme=theme) post.add_hook("question", hook) comparison = f"{option_a}||{option_b}" post.add_body(comparison, structure="comparison") post.add_separator("line") post.add_body(f"My take: {recommendation}", structure="linear") post.add_cta("curiosity", "Which would you choose?") return post

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