zzstoatzz commited on
Commit
bbbf830
·
1 Parent(s): 619046f
examples/atproto_mcp/README.md CHANGED
@@ -13,6 +13,7 @@ This example demonstrates a FastMCP server that provides tools and resources for
13
  ### Tools (Actions)
14
 
15
  - **post**: Create posts with rich features (text, images, quotes, replies, links, mentions)
 
16
  - **search**: Search for posts by query
17
  - **follow**: Follow users by handle
18
  - **like**: Like posts by URI
@@ -107,6 +108,15 @@ async def demo():
107
  "reply_to": "at://did:plc:xxx/app.bsky.feed.post/yyy",
108
  "links": [{"text": "this article", "url": "https://example.com/article"}]
109
  })
 
 
 
 
 
 
 
 
 
110
  ```
111
 
112
  ## AI Assistant Use Cases
@@ -116,7 +126,7 @@ The unified API enables natural AI assistant interactions:
116
  - **"Reply to that post with these findings"** → Uses `reply_to` with rich text
117
  - **"Share this article with commentary"** → Uses `quote` with the article link
118
  - **"Post this chart with explanation"** → Uses `images` with descriptive text
119
- - **"Start a thread about AI safety"** → Chain multiple posts with `reply_to`
120
 
121
  ## Architecture
122
 
 
13
  ### Tools (Actions)
14
 
15
  - **post**: Create posts with rich features (text, images, quotes, replies, links, mentions)
16
+ - **create_thread**: Post multi-part threads with automatic linking
17
  - **search**: Search for posts by query
18
  - **follow**: Follow users by handle
19
  - **like**: Like posts by URI
 
108
  "reply_to": "at://did:plc:xxx/app.bsky.feed.post/yyy",
109
  "links": [{"text": "this article", "url": "https://example.com/article"}]
110
  })
111
+
112
+ # Create a thread
113
+ await client.call_tool("create_thread", {
114
+ "posts": [
115
+ {"text": "Starting a thread about Python 🧵"},
116
+ {"text": "Python is great for rapid prototyping"},
117
+ {"text": "And the ecosystem is amazing!", "images": ["https://example.com/python.jpg"]}
118
+ ]
119
+ })
120
  ```
121
 
122
  ## AI Assistant Use Cases
 
126
  - **"Reply to that post with these findings"** → Uses `reply_to` with rich text
127
  - **"Share this article with commentary"** → Uses `quote` with the article link
128
  - **"Post this chart with explanation"** → Uses `images` with descriptive text
129
+ - **"Start a thread about AI safety"** → Uses `create_thread` for automatic linking
130
 
131
  ## Architecture
132
 
examples/atproto_mcp/demo.py CHANGED
@@ -185,6 +185,37 @@ async def main(enable_posting: bool = False):
185
  )
186
  if json.loads(result[0].text).get("success"):
187
  print(" ✅ Followed @alternatebuild.dev!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  else:
189
  print("\n5. Posting capabilities (not enabled):")
190
  print(" To test posting, run with --post flag")
@@ -206,6 +237,7 @@ async def main(enable_posting: bool = False):
206
  print(" • Quote posts")
207
  print(" • Combinations (quote + image, reply + rich text, etc.)")
208
  print(" - search: Search for posts")
 
209
  print(" - follow: Follow users")
210
  print(" - like: Like posts")
211
  print(" - repost: Share posts")
 
185
  )
186
  if json.loads(result[0].text).get("success"):
187
  print(" ✅ Followed @alternatebuild.dev!")
188
+
189
+ # h. Thread creation (new!)
190
+ print("\n h) Creating a thread...")
191
+ result = await client.call_tool(
192
+ "create_thread",
193
+ {
194
+ "posts": [
195
+ {
196
+ "text": "Let me share some thoughts about the ATProto MCP server 🧵"
197
+ },
198
+ {
199
+ "text": "First, it makes posting from the terminal incredibly smooth"
200
+ },
201
+ {
202
+ "text": "The unified post API means one tool handles everything",
203
+ "links": [
204
+ {
205
+ "text": "everything",
206
+ "url": "https://github.com/jlowin/fastmcp",
207
+ }
208
+ ],
209
+ },
210
+ {
211
+ "text": "And now with create_thread, multi-post threads are trivial!"
212
+ },
213
+ ]
214
+ },
215
+ )
216
+ if json.loads(result[0].text).get("success"):
217
+ thread_result = json.loads(result[0].text)
218
+ print(f" ✅ Thread created with {thread_result['post_count']} posts!")
219
  else:
220
  print("\n5. Posting capabilities (not enabled):")
221
  print(" To test posting, run with --post flag")
 
237
  print(" • Quote posts")
238
  print(" • Combinations (quote + image, reply + rich text, etc.)")
239
  print(" - search: Search for posts")
240
+ print(" - create_thread: Post multi-part threads")
241
  print(" - follow: Follow users")
242
  print(" - like: Like posts")
243
  print(" - repost: Share posts")
examples/atproto_mcp/src/atproto_mcp/_atproto/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
  """Private ATProto implementation module."""
2
 
3
  from ._client import get_client
4
- from ._posts import create_post
5
  from ._profile import get_profile_info
6
  from ._read import fetch_notifications, fetch_timeline, search_for_posts
7
  from ._social import follow_user_by_handle, like_post_by_uri, repost_by_uri
@@ -10,6 +10,7 @@ __all__ = [
10
  "get_client",
11
  "get_profile_info",
12
  "create_post",
 
13
  "fetch_timeline",
14
  "search_for_posts",
15
  "fetch_notifications",
 
1
  """Private ATProto implementation module."""
2
 
3
  from ._client import get_client
4
+ from ._posts import create_post, create_thread
5
  from ._profile import get_profile_info
6
  from ._read import fetch_notifications, fetch_timeline, search_for_posts
7
  from ._social import follow_user_by_handle, like_post_by_uri, repost_by_uri
 
10
  "get_client",
11
  "get_profile_info",
12
  "create_post",
13
+ "create_thread",
14
  "fetch_timeline",
15
  "search_for_posts",
16
  "fetch_notifications",
examples/atproto_mcp/src/atproto_mcp/_atproto/_posts.py CHANGED
@@ -4,7 +4,13 @@ from datetime import datetime
4
 
5
  from atproto import models
6
 
7
- from atproto_mcp.types import PostResult, RichTextLink, RichTextMention
 
 
 
 
 
 
8
 
9
  from ._client import get_client
10
 
@@ -282,3 +288,90 @@ def _send_images(
282
  created_at=datetime.now().isoformat(),
283
  error=None,
284
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  from atproto import models
6
 
7
+ from atproto_mcp.types import (
8
+ PostResult,
9
+ RichTextLink,
10
+ RichTextMention,
11
+ ThreadPost,
12
+ ThreadResult,
13
+ )
14
 
15
  from ._client import get_client
16
 
 
288
  created_at=datetime.now().isoformat(),
289
  error=None,
290
  )
291
+
292
+
293
+ def create_thread(posts: list[ThreadPost]) -> ThreadResult:
294
+ """Create a thread of posts with automatic linking.
295
+
296
+ Args:
297
+ posts: List of posts to create as a thread. First post is the root.
298
+ """
299
+ if not posts:
300
+ return ThreadResult(
301
+ success=False,
302
+ thread_uri=None,
303
+ post_uris=[],
304
+ post_count=0,
305
+ error="No posts provided",
306
+ )
307
+
308
+ try:
309
+ post_uris = []
310
+ root_uri = None
311
+ parent_uri = None
312
+
313
+ for i, post_data in enumerate(posts):
314
+ # First post is the root
315
+ if i == 0:
316
+ result = create_post(
317
+ text=post_data["text"],
318
+ images=post_data.get("images"),
319
+ image_alts=post_data.get("image_alts"),
320
+ links=post_data.get("links"),
321
+ mentions=post_data.get("mentions"),
322
+ quote=post_data.get("quote"),
323
+ )
324
+
325
+ if not result["success"]:
326
+ return ThreadResult(
327
+ success=False,
328
+ thread_uri=None,
329
+ post_uris=post_uris,
330
+ post_count=len(post_uris),
331
+ error=f"Failed to create root post: {result['error']}",
332
+ )
333
+
334
+ root_uri = result["uri"]
335
+ parent_uri = root_uri
336
+ post_uris.append(root_uri)
337
+ else:
338
+ # Subsequent posts reply to the previous one
339
+ result = create_post(
340
+ text=post_data["text"],
341
+ images=post_data.get("images"),
342
+ image_alts=post_data.get("image_alts"),
343
+ links=post_data.get("links"),
344
+ mentions=post_data.get("mentions"),
345
+ quote=post_data.get("quote"),
346
+ reply_to=parent_uri,
347
+ reply_root=root_uri,
348
+ )
349
+
350
+ if not result["success"]:
351
+ return ThreadResult(
352
+ success=False,
353
+ thread_uri=root_uri,
354
+ post_uris=post_uris,
355
+ post_count=len(post_uris),
356
+ error=f"Failed to create post {i + 1}: {result['error']}",
357
+ )
358
+
359
+ parent_uri = result["uri"]
360
+ post_uris.append(parent_uri)
361
+
362
+ return ThreadResult(
363
+ success=True,
364
+ thread_uri=root_uri,
365
+ post_uris=post_uris,
366
+ post_count=len(post_uris),
367
+ error=None,
368
+ )
369
+
370
+ except Exception as e:
371
+ return ThreadResult(
372
+ success=False,
373
+ thread_uri=None,
374
+ post_uris=post_uris,
375
+ post_count=len(post_uris),
376
+ error=str(e),
377
+ )
examples/atproto_mcp/src/atproto_mcp/server.py CHANGED
@@ -16,6 +16,8 @@ from atproto_mcp.types import (
16
  RichTextLink,
17
  RichTextMention,
18
  SearchResult,
 
 
19
  TimelineResult,
20
  )
21
  from fastmcp import FastMCP
@@ -126,3 +128,27 @@ def search(
126
  ) -> SearchResult:
127
  """Search for posts containing specific text."""
128
  return _atproto.search_for_posts(query, limit)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  RichTextLink,
17
  RichTextMention,
18
  SearchResult,
19
+ ThreadPost,
20
+ ThreadResult,
21
  TimelineResult,
22
  )
23
  from fastmcp import FastMCP
 
128
  ) -> SearchResult:
129
  """Search for posts containing specific text."""
130
  return _atproto.search_for_posts(query, limit)
131
+
132
+
133
+ @atproto_mcp.tool
134
+ def create_thread(
135
+ posts: Annotated[
136
+ list[ThreadPost],
137
+ Field(
138
+ description="List of posts to create as a thread. Each post can have text, images, links, mentions, and quotes."
139
+ ),
140
+ ],
141
+ ) -> ThreadResult:
142
+ """Create a thread of posts with automatic linking.
143
+
144
+ The first post becomes the root of the thread, and each subsequent post
145
+ replies to the previous one, maintaining the thread structure.
146
+
147
+ Example:
148
+ create_thread([
149
+ {"text": "Starting a thread about Python 🧵"},
150
+ {"text": "Python is great for rapid development"},
151
+ {"text": "And the ecosystem is amazing!", "images": ["https://example.com/python.jpg"]}
152
+ ])
153
+ """
154
+ return _atproto.create_thread(posts)
examples/atproto_mcp/src/atproto_mcp/types.py CHANGED
@@ -119,3 +119,24 @@ class RichTextMention(TypedDict):
119
 
120
  handle: str
121
  display_text: str | None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
  handle: str
121
  display_text: str | None
122
+
123
+
124
+ class ThreadPost(TypedDict, total=False):
125
+ """A post in a thread."""
126
+
127
+ text: str # Required
128
+ images: list[str] | None
129
+ image_alts: list[str] | None
130
+ links: list[RichTextLink] | None
131
+ mentions: list[RichTextMention] | None
132
+ quote: str | None
133
+
134
+
135
+ class ThreadResult(TypedDict):
136
+ """Result of creating a thread."""
137
+
138
+ success: bool
139
+ thread_uri: str | None # URI of the first post
140
+ post_uris: list[str]
141
+ post_count: int
142
+ error: str | None
examples/atproto_mcp/uv.lock ADDED
The diff for this file is too large to render. See raw diff