From 5fd8ba1aa4184db75ae9c746d86157bac9c4f815 Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Wed, 20 Aug 2025 00:36:16 -0500 Subject: [PATCH 1/3] reauth client --- examples/atproto_mcp/src/atproto_mcp/_atproto/_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/atproto_mcp/src/atproto_mcp/_atproto/_client.py b/examples/atproto_mcp/src/atproto_mcp/_atproto/_client.py index 40ee8e160..8acce18ce 100644 --- a/examples/atproto_mcp/src/atproto_mcp/_atproto/_client.py +++ b/examples/atproto_mcp/src/atproto_mcp/_atproto/_client.py @@ -12,5 +12,5 @@ def get_client() -> Client: global _client if _client is None: _client = Client() - _client.login(settings.atproto_handle, settings.atproto_password) + _client.login(settings.atproto_handle, settings.atproto_password) return _client From d3f120a05975ff90274d39ff8d0ea51615938269 Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Wed, 20 Aug 2025 00:54:09 -0500 Subject: [PATCH 2/3] undo unnecessary client change --- examples/atproto_mcp/src/atproto_mcp/_atproto/_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/atproto_mcp/src/atproto_mcp/_atproto/_client.py b/examples/atproto_mcp/src/atproto_mcp/_atproto/_client.py index 8acce18ce..40ee8e160 100644 --- a/examples/atproto_mcp/src/atproto_mcp/_atproto/_client.py +++ b/examples/atproto_mcp/src/atproto_mcp/_atproto/_client.py @@ -12,5 +12,5 @@ def get_client() -> Client: global _client if _client is None: _client = Client() - _client.login(settings.atproto_handle, settings.atproto_password) + _client.login(settings.atproto_handle, settings.atproto_password) return _client From 04efab4257afb5b839d35d4a34990d56618eac52 Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Wed, 20 Aug 2025 11:31:56 -0500 Subject: [PATCH 3/3] fix: auto-detect URLs in ATProto posts for clickable links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ATProto MCP server now properly handles URLs in posts by: - Auto-detecting plain URLs in text and creating facets to make them clickable - Supporting rich text links with custom anchor text - Preventing overlap between auto-detected and explicitly provided links This ensures all URLs are clickable on Bluesky, whether they're plain URLs in text or rich text links with custom anchors. 🤖 Generated with Claude Code Co-Authored-By: Claude --- .../src/atproto_mcp/_atproto/_posts.py | 51 ++++++++++++++++--- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/examples/atproto_mcp/src/atproto_mcp/_atproto/_posts.py b/examples/atproto_mcp/src/atproto_mcp/_atproto/_posts.py index e7a5b7dbd..daf8c340a 100644 --- a/examples/atproto_mcp/src/atproto_mcp/_atproto/_posts.py +++ b/examples/atproto_mcp/src/atproto_mcp/_atproto/_posts.py @@ -44,9 +44,8 @@ def create_post( embed = None reply_ref = None - # Handle rich text facets (links and mentions) - if links or mentions: - facets = _build_facets(text, links, mentions, client) + # Always build facets to handle auto-detected URLs and explicit links/mentions + facets = _build_facets(text, links, mentions, client) # Handle replies if reply_to: @@ -63,7 +62,7 @@ def create_post( # Images only - use send_images for proper handling return _send_images(text, images, image_alts, facets, reply_ref, client) - # Send the post + # Send the post (always include facets if any were created) post = client.send_post( text=text, facets=facets if facets else None, @@ -96,10 +95,18 @@ def _build_facets( mentions: list[RichTextMention] | None, client, ): - """Build facets for rich text formatting.""" - facets = [] + """Build facets for rich text formatting, including auto-detected URLs.""" + import re - # Process links + facets = [] + covered_ranges = [] + + # URL regex pattern for auto-detection + url_pattern = re.compile( + r"https?://(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&/=]*)" + ) + + # Process explicit links first if links: for link in links: start = text.find(link["text"]) @@ -107,6 +114,9 @@ def _build_facets( continue end = start + len(link["text"]) + # Track this range as covered + covered_ranges.append((start, end)) + facets.append( models.AppBskyRichtextFacet.Main( features=[models.AppBskyRichtextFacet.Link(uri=link["url"])], @@ -117,6 +127,30 @@ def _build_facets( ) ) + # Auto-detect URLs that aren't already covered by explicit links + for match in url_pattern.finditer(text): + url = match.group() + start = match.start() + end = match.end() + + # Check if this URL overlaps with any explicit link + overlaps = False + for covered_start, covered_end in covered_ranges: + if not (end <= covered_start or start >= covered_end): + overlaps = True + break + + if not overlaps: + facets.append( + models.AppBskyRichtextFacet.Main( + features=[models.AppBskyRichtextFacet.Link(uri=url)], + index=models.AppBskyRichtextFacet.ByteSlice( + byte_start=len(text[:start].encode("UTF-8")), + byte_end=len(text[:end].encode("UTF-8")), + ), + ) + ) + # Process mentions if mentions: for mention in mentions: @@ -253,7 +287,8 @@ def _send_images( # Send post with images # Note: send_images doesn't support facets or reply_to directly - # So we need to use send_post with manual image upload if we have those + # So we need to use send_post with manual image upload if we have facets or replies + # Since we always create facets now (for URL auto-detection), we'll always use this path if facets or reply_ref: # Manual image upload images = []