Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5ecfd5e
chore: Update repository URLs to getlatedev/late-python-sdk
carlosmgv02 Dec 11, 2025
c75c624
fix: Resolve lint errors and update MCP test imports
carlosmgv02 Dec 11, 2025
49cf5dc
fix: Resolve mypy errors and add params to HTTP methods
carlosmgv02 Dec 11, 2025
5085c5f
ci: Improve release workflow with better visibility and PyPI check
carlosmgv02 Dec 11, 2025
bf3071b
ci: Add release preview comment on PRs to main
carlosmgv02 Dec 11, 2025
4360447
chore: trigger workflow re-run
carlosmgv02 Dec 11, 2025
359e5bb
fix: Add explicit permissions for checkout in private repo
carlosmgv02 Dec 11, 2025
842ca46
fix: Add explicit token and permissions to all workflows for private …
carlosmgv02 Dec 11, 2025
061ac30
feat: Add typed responses with Pydantic models (v1.1.0)
carlosmgv02 Dec 15, 2025
a98b89b
feat(mcp): Add is_draft parameter and centralized tool definitions
carlosmgv02 Dec 15, 2025
e2db7a4
fix: Move Callable imports to TYPE_CHECKING block and fix trailing wh…
carlosmgv02 Dec 15, 2025
5b5abba
fix: Fix mypy errors and format code
carlosmgv02 Dec 15, 2025
a22f903
Merge branch 'main' into develop
carlosmgv02 Dec 15, 2025
33f0933
feat(ai): Add model property to OpenAI provider
carlosmgv02 Dec 15, 2025
72f764d
Refactor MCP server to use typed models and tool docs
carlosmgv02 Dec 15, 2025
d99d54d
Merge branch 'main' into develop
carlosmgv02 Dec 15, 2025
529ab35
Update lint config and import Any in server.py
carlosmgv02 Dec 16, 2025
c461bd7
feat(mcp): Add docs_search tool for documentation search
carlosmgv02 Jan 8, 2026
4a19cac
Merge branch 'main' into develop
carlosmgv02 Jan 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "late-sdk"
version = "1.1.1"
version = "1.1.2"
description = "Python SDK for Late API - Social Media Scheduling"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
116 changes: 116 additions & 0 deletions src/late/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,22 @@
from __future__ import annotations

import os
import re
from datetime import datetime, timedelta
from typing import Any

import httpx
from mcp.server.fastmcp import FastMCP

from late import Late, MediaType, PostStatus

from .tool_definitions import use_tool_def

# Cache for documentation content
_docs_cache: dict[str, tuple[str, datetime]] = {}
_DOCS_URL = "https://docs.getlate.dev/llms-full.txt"
_CACHE_TTL_HOURS = 24

# Initialize MCP server
mcp = FastMCP(
"Late",
Expand All @@ -44,6 +51,7 @@
- profiles_* : Manage profiles (groups of accounts)
- posts_* : Create, list, update, delete posts
- media_* : Upload images and videos
- docs_* : Search Late API documentation
""",
)

Expand Down Expand Up @@ -635,6 +643,114 @@ def media_check_upload_status(token: str) -> str:
return f"❌ Failed to check upload status: {e}"


# ============================================================================
# DOCS
# ============================================================================


def _get_docs_content() -> str:
"""Fetch and cache documentation content."""
cache_key = "docs"

# Check cache
if cache_key in _docs_cache:
content, cached_at = _docs_cache[cache_key]
if datetime.now() - cached_at < timedelta(hours=_CACHE_TTL_HOURS):
return content

# Fetch fresh content
try:
response = httpx.get(_DOCS_URL, timeout=30.0)
response.raise_for_status()
content = response.text
_docs_cache[cache_key] = (content, datetime.now())
return content
except Exception as e:
# Return cached content if available, even if expired
if cache_key in _docs_cache:
return _docs_cache[cache_key][0]
raise RuntimeError(f"Failed to fetch documentation: {e}") from e


def _search_docs(content: str, query: str, max_results: int = 5) -> list[dict[str, str]]:
"""Search documentation content for relevant sections."""
results: list[dict[str, str]] = []
query_lower = query.lower()
query_terms = query_lower.split()

# Split content into sections (by markdown headers)
sections = re.split(r'\n(?=#{1,3} )', content)

scored_sections: list[tuple[int, str, str]] = []

for section in sections:
if not section.strip():
continue

section_lower = section.lower()

# Calculate relevance score
score = 0

# Exact phrase match (highest priority)
if query_lower in section_lower:
score += 100

# Individual term matches
for term in query_terms:
if term in section_lower:
score += 10
# Bonus for term in header
first_line = section.split('\n')[0].lower()
if term in first_line:
score += 20

if score > 0:
# Extract title from first line
lines = section.strip().split('\n')
title = lines[0].lstrip('#').strip() if lines else "Untitled"
scored_sections.append((score, title, section.strip()))

# Sort by score and take top results
scored_sections.sort(key=lambda x: x[0], reverse=True)

for score, title, section_text in scored_sections[:max_results]:
# Truncate long sections
if len(section_text) > 1500:
section_text = section_text[:1500] + "\n...(truncated)"

results.append({
"title": title,
"content": section_text,
"relevance": str(score),
})

return results


@mcp.tool()
@use_tool_def("docs_search")
def docs_search(query: str) -> str:
try:
content = _get_docs_content()
results = _search_docs(content, query)

if not results:
return f"No documentation found for '{query}'. Try different search terms."

lines = [f"Found {len(results)} relevant section(s) for '{query}':\n"]

for i, result in enumerate(results, 1):
lines.append(f"--- Result {i}: {result['title']} ---")
lines.append(result["content"])
lines.append("")

return "\n".join(lines)

except Exception as e:
return f"❌ Failed to search documentation: {e}"


# ============================================================================
# MAIN
# ============================================================================
Expand Down
25 changes: 25 additions & 0 deletions src/late/mcp/tool_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,28 @@ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
params=[],
)

# =============================================================================
# DOCS TOOLS
# =============================================================================

DOCS_SEARCH = ToolDef(
name="docs_search",
summary="Search the Late API documentation.",
description="""Search across the Late API documentation to find relevant information, code examples, API references, and guides.

Use this tool when you need to answer questions about Late, find specific documentation, understand how features work, or locate implementation details.

The search returns contextual content with section titles and relevant snippets.""",
params=[
ParamDef(
name="query",
type="str",
description="Search query (e.g., 'webhooks', 'create post', 'authentication')",
required=True,
),
],
)

# =============================================================================
# MEDIA TOOLS
# =============================================================================
Expand Down Expand Up @@ -608,6 +630,8 @@ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
# Media
"media_generate_upload_link": MEDIA_GENERATE_UPLOAD_LINK,
"media_check_upload_status": MEDIA_CHECK_UPLOAD_STATUS,
# Docs
"docs_search": DOCS_SEARCH,
}


Expand Down Expand Up @@ -648,6 +672,7 @@ def generate_mdx_tools_reference() -> str:
"posts_retry_all_failed",
],
"Media": ["media_generate_upload_link", "media_check_upload_status"],
"Docs": ["docs_search"],
}

for category, tool_names in categories.items():
Expand Down