Skip to content

Commit c461bd7

Browse files
committed
feat(mcp): Add docs_search tool for documentation search
- Add docs_search tool to search Late API documentation - Fetch and cache llms-full.txt with 24h TTL - Score-based search across markdown sections - Returns top 5 relevant documentation sections
1 parent 529ab35 commit c461bd7

3 files changed

Lines changed: 142 additions & 1 deletion

File tree

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "late-sdk"
3-
version = "1.1.1"
3+
version = "1.1.2"
44
description = "Python SDK for Late API - Social Media Scheduling"
55
readme = "README.md"
66
requires-python = ">=3.10"

‎src/late/mcp/server.py‎

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,22 @@
2424
from __future__ import annotations
2525

2626
import os
27+
import re
2728
from datetime import datetime, timedelta
2829
from typing import Any
2930

31+
import httpx
3032
from mcp.server.fastmcp import FastMCP
3133

3234
from late import Late, MediaType, PostStatus
3335

3436
from .tool_definitions import use_tool_def
3537

38+
# Cache for documentation content
39+
_docs_cache: dict[str, tuple[str, datetime]] = {}
40+
_DOCS_URL = "https://docs.getlate.dev/llms-full.txt"
41+
_CACHE_TTL_HOURS = 24
42+
3643
# Initialize MCP server
3744
mcp = FastMCP(
3845
"Late",
@@ -44,6 +51,7 @@
4451
- profiles_* : Manage profiles (groups of accounts)
4552
- posts_* : Create, list, update, delete posts
4653
- media_* : Upload images and videos
54+
- docs_* : Search Late API documentation
4755
""",
4856
)
4957

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

637645

646+
# ============================================================================
647+
# DOCS
648+
# ============================================================================
649+
650+
651+
def _get_docs_content() -> str:
652+
"""Fetch and cache documentation content."""
653+
cache_key = "docs"
654+
655+
# Check cache
656+
if cache_key in _docs_cache:
657+
content, cached_at = _docs_cache[cache_key]
658+
if datetime.now() - cached_at < timedelta(hours=_CACHE_TTL_HOURS):
659+
return content
660+
661+
# Fetch fresh content
662+
try:
663+
response = httpx.get(_DOCS_URL, timeout=30.0)
664+
response.raise_for_status()
665+
content = response.text
666+
_docs_cache[cache_key] = (content, datetime.now())
667+
return content
668+
except Exception as e:
669+
# Return cached content if available, even if expired
670+
if cache_key in _docs_cache:
671+
return _docs_cache[cache_key][0]
672+
raise RuntimeError(f"Failed to fetch documentation: {e}") from e
673+
674+
675+
def _search_docs(content: str, query: str, max_results: int = 5) -> list[dict[str, str]]:
676+
"""Search documentation content for relevant sections."""
677+
results: list[dict[str, str]] = []
678+
query_lower = query.lower()
679+
query_terms = query_lower.split()
680+
681+
# Split content into sections (by markdown headers)
682+
sections = re.split(r'\n(?=#{1,3} )', content)
683+
684+
scored_sections: list[tuple[int, str, str]] = []
685+
686+
for section in sections:
687+
if not section.strip():
688+
continue
689+
690+
section_lower = section.lower()
691+
692+
# Calculate relevance score
693+
score = 0
694+
695+
# Exact phrase match (highest priority)
696+
if query_lower in section_lower:
697+
score += 100
698+
699+
# Individual term matches
700+
for term in query_terms:
701+
if term in section_lower:
702+
score += 10
703+
# Bonus for term in header
704+
first_line = section.split('\n')[0].lower()
705+
if term in first_line:
706+
score += 20
707+
708+
if score > 0:
709+
# Extract title from first line
710+
lines = section.strip().split('\n')
711+
title = lines[0].lstrip('#').strip() if lines else "Untitled"
712+
scored_sections.append((score, title, section.strip()))
713+
714+
# Sort by score and take top results
715+
scored_sections.sort(key=lambda x: x[0], reverse=True)
716+
717+
for score, title, section_text in scored_sections[:max_results]:
718+
# Truncate long sections
719+
if len(section_text) > 1500:
720+
section_text = section_text[:1500] + "\n...(truncated)"
721+
722+
results.append({
723+
"title": title,
724+
"content": section_text,
725+
"relevance": str(score),
726+
})
727+
728+
return results
729+
730+
731+
@mcp.tool()
732+
@use_tool_def("docs_search")
733+
def docs_search(query: str) -> str:
734+
try:
735+
content = _get_docs_content()
736+
results = _search_docs(content, query)
737+
738+
if not results:
739+
return f"No documentation found for '{query}'. Try different search terms."
740+
741+
lines = [f"Found {len(results)} relevant section(s) for '{query}':\n"]
742+
743+
for i, result in enumerate(results, 1):
744+
lines.append(f"--- Result {i}: {result['title']} ---")
745+
lines.append(result["content"])
746+
lines.append("")
747+
748+
return "\n".join(lines)
749+
750+
except Exception as e:
751+
return f"❌ Failed to search documentation: {e}"
752+
753+
638754
# ============================================================================
639755
# MAIN
640756
# ============================================================================

‎src/late/mcp/tool_definitions.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,28 @@ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
543543
params=[],
544544
)
545545

546+
# =============================================================================
547+
# DOCS TOOLS
548+
# =============================================================================
549+
550+
DOCS_SEARCH = ToolDef(
551+
name="docs_search",
552+
summary="Search the Late API documentation.",
553+
description="""Search across the Late API documentation to find relevant information, code examples, API references, and guides.
554+
555+
Use this tool when you need to answer questions about Late, find specific documentation, understand how features work, or locate implementation details.
556+
557+
The search returns contextual content with section titles and relevant snippets.""",
558+
params=[
559+
ParamDef(
560+
name="query",
561+
type="str",
562+
description="Search query (e.g., 'webhooks', 'create post', 'authentication')",
563+
required=True,
564+
),
565+
],
566+
)
567+
546568
# =============================================================================
547569
# MEDIA TOOLS
548570
# =============================================================================
@@ -608,6 +630,8 @@ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
608630
# Media
609631
"media_generate_upload_link": MEDIA_GENERATE_UPLOAD_LINK,
610632
"media_check_upload_status": MEDIA_CHECK_UPLOAD_STATUS,
633+
# Docs
634+
"docs_search": DOCS_SEARCH,
611635
}
612636

613637

@@ -648,6 +672,7 @@ def generate_mdx_tools_reference() -> str:
648672
"posts_retry_all_failed",
649673
],
650674
"Media": ["media_generate_upload_link", "media_check_upload_status"],
675+
"Docs": ["docs_search"],
651676
}
652677

653678
for category, tool_names in categories.items():

0 commit comments

Comments
 (0)