Spaces:
Sleeping
Sleeping
zzstoatzz commited on
Commit ·
1fc02fb
1
Parent(s): 2a85f54
Implement full ATProto functionality with 8 tools for Bluesky interaction
Browse files- examples/atproto_mcp/README.md +70 -0
- examples/atproto_mcp/demo.py +101 -0
- examples/atproto_mcp/src/atproto_mcp/server.py +196 -12
examples/atproto_mcp/README.md
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ATProto MCP Server
|
| 2 |
+
|
| 3 |
+
This example demonstrates a FastMCP server that provides tools for interacting with the AT Protocol (Bluesky).
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
The server provides the following tools:
|
| 8 |
+
|
| 9 |
+
- **atproto_status**: Check connection status and profile information
|
| 10 |
+
- **post_to_bluesky**: Create new posts on Bluesky
|
| 11 |
+
- **get_timeline**: Retrieve your timeline feed
|
| 12 |
+
- **search_posts**: Search for posts by keyword
|
| 13 |
+
- **get_notifications**: Get recent notifications
|
| 14 |
+
- **follow_user**: Follow a user by handle
|
| 15 |
+
- **like_post**: Like a post by URI
|
| 16 |
+
- **repost**: Repost content by URI
|
| 17 |
+
|
| 18 |
+
## Setup
|
| 19 |
+
|
| 20 |
+
1. Create a `.env` file in the root directory with your Bluesky credentials:
|
| 21 |
+
|
| 22 |
+
```bash
|
| 23 |
+
ATPROTO_HANDLE=your.handle@bsky.social
|
| 24 |
+
ATPROTO_PASSWORD=your-app-password
|
| 25 |
+
ATPROTO_PDS_URL=https://bsky.social # optional, defaults to bsky.social
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
2. Install and run the server:
|
| 29 |
+
|
| 30 |
+
```bash
|
| 31 |
+
# Install dependencies
|
| 32 |
+
pip install -e .
|
| 33 |
+
|
| 34 |
+
# Run the server
|
| 35 |
+
python -m atproto_mcp
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
## Usage Example
|
| 39 |
+
|
| 40 |
+
```python
|
| 41 |
+
from fastmcp import Client
|
| 42 |
+
from atproto_mcp.server import atproto_mcp
|
| 43 |
+
|
| 44 |
+
async def demo():
|
| 45 |
+
async with Client(atproto_mcp) as client:
|
| 46 |
+
# Check status
|
| 47 |
+
status = await client.call_tool("atproto_status", {})
|
| 48 |
+
print(f"Connected as: {status['handle']}")
|
| 49 |
+
|
| 50 |
+
# Post to Bluesky
|
| 51 |
+
post = await client.call_tool("post_to_bluesky", {
|
| 52 |
+
"text": "Hello from FastMCP! =�"
|
| 53 |
+
})
|
| 54 |
+
print(f"Posted: {post['uri']}")
|
| 55 |
+
|
| 56 |
+
# Get timeline
|
| 57 |
+
timeline = await client.call_tool("get_timeline", {"limit": 5})
|
| 58 |
+
print(f"Found {timeline['count']} posts")
|
| 59 |
+
|
| 60 |
+
# Search posts
|
| 61 |
+
results = await client.call_tool("search_posts", {
|
| 62 |
+
"query": "FastMCP",
|
| 63 |
+
"limit": 10
|
| 64 |
+
})
|
| 65 |
+
print(f"Found {results['count']} posts matching 'FastMCP'")
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
## Security Note
|
| 69 |
+
|
| 70 |
+
Store your Bluesky credentials securely in environment variables. Never commit credentials to version control.
|
examples/atproto_mcp/demo.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Demo script showing ATProto MCP server capabilities.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import asyncio
|
| 7 |
+
import sys
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
# Add the src directory to the path
|
| 11 |
+
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
| 12 |
+
|
| 13 |
+
from atproto_mcp.server import atproto_mcp
|
| 14 |
+
|
| 15 |
+
from fastmcp import Client
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
async def main():
|
| 19 |
+
print("🔵 ATProto MCP Server Demo\n")
|
| 20 |
+
|
| 21 |
+
async with Client(atproto_mcp) as client:
|
| 22 |
+
# 1. Check connection status
|
| 23 |
+
print("1. Checking connection status...")
|
| 24 |
+
status = await client.call_tool("atproto_status", {})
|
| 25 |
+
|
| 26 |
+
if status.get("connected"):
|
| 27 |
+
print(f"✅ Connected as: @{status['handle']}")
|
| 28 |
+
print(f" Followers: {status['followers']}")
|
| 29 |
+
print(f" Following: {status['following']}")
|
| 30 |
+
print(f" Posts: {status['posts']}")
|
| 31 |
+
else:
|
| 32 |
+
print(f"❌ Connection failed: {status.get('error')}")
|
| 33 |
+
return
|
| 34 |
+
|
| 35 |
+
# 2. Get timeline
|
| 36 |
+
print("\n2. Getting timeline (last 3 posts)...")
|
| 37 |
+
timeline = await client.call_tool("get_timeline", {"limit": 3})
|
| 38 |
+
|
| 39 |
+
if timeline.get("success"):
|
| 40 |
+
print(f"✅ Found {timeline['count']} posts:")
|
| 41 |
+
for i, post in enumerate(timeline["posts"], 1):
|
| 42 |
+
print(f"\n Post {i}:")
|
| 43 |
+
print(f" Author: @{post['author']}")
|
| 44 |
+
print(
|
| 45 |
+
f" Text: {post['text'][:100]}..."
|
| 46 |
+
if post["text"] and len(post["text"]) > 100
|
| 47 |
+
else f" Text: {post['text']}"
|
| 48 |
+
)
|
| 49 |
+
print(
|
| 50 |
+
f" Likes: {post['likes']} | Reposts: {post['reposts']} | Replies: {post['replies']}"
|
| 51 |
+
)
|
| 52 |
+
else:
|
| 53 |
+
print(f"❌ Failed to get timeline: {timeline.get('error')}")
|
| 54 |
+
|
| 55 |
+
# 3. Search for posts
|
| 56 |
+
print("\n3. Searching for posts about 'Python'...")
|
| 57 |
+
search = await client.call_tool("search_posts", {"query": "Python", "limit": 3})
|
| 58 |
+
|
| 59 |
+
if search.get("success"):
|
| 60 |
+
print(f"✅ Found {search['count']} posts about Python")
|
| 61 |
+
if search["posts"]:
|
| 62 |
+
post = search["posts"][0]
|
| 63 |
+
print(
|
| 64 |
+
f" Latest by @{post['author']}: {post['text'][:100]}..."
|
| 65 |
+
if post["text"] and len(post["text"]) > 100
|
| 66 |
+
else f" Latest by @{post['author']}: {post['text']}"
|
| 67 |
+
)
|
| 68 |
+
else:
|
| 69 |
+
print(f"❌ Search failed: {search.get('error')}")
|
| 70 |
+
|
| 71 |
+
# 4. Get notifications
|
| 72 |
+
print("\n4. Checking notifications...")
|
| 73 |
+
notifs = await client.call_tool("get_notifications", {"limit": 5})
|
| 74 |
+
|
| 75 |
+
if notifs.get("success"):
|
| 76 |
+
print(f"✅ You have {notifs['count']} recent notifications")
|
| 77 |
+
unread = sum(1 for n in notifs["notifications"] if not n["is_read"])
|
| 78 |
+
if unread:
|
| 79 |
+
print(f" ({unread} unread)")
|
| 80 |
+
else:
|
| 81 |
+
print(f"❌ Failed to get notifications: {notifs.get('error')}")
|
| 82 |
+
|
| 83 |
+
# 5. Demo posting (commented out to avoid spam)
|
| 84 |
+
print("\n5. Posting capability:")
|
| 85 |
+
print(" To post, you would use:")
|
| 86 |
+
print(
|
| 87 |
+
' await client.call_tool("post_to_bluesky", {"text": "Hello from FastMCP! 🚀"})'
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
# Uncomment to actually post:
|
| 91 |
+
# post = await client.call_tool("post_to_bluesky", {
|
| 92 |
+
# "text": "Testing ATProto MCP server with FastMCP! 🚀"
|
| 93 |
+
# })
|
| 94 |
+
# if post.get("success"):
|
| 95 |
+
# print(f"✅ Posted successfully: {post['uri']}")
|
| 96 |
+
|
| 97 |
+
print("\n✨ Demo complete!")
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
if __name__ == "__main__":
|
| 101 |
+
asyncio.run(main())
|
examples/atproto_mcp/src/atproto_mcp/server.py
CHANGED
|
@@ -1,26 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from atproto_mcp.settings import settings
|
| 2 |
from fastmcp import FastMCP
|
| 3 |
|
| 4 |
atproto_mcp = FastMCP(
|
| 5 |
"ATProto MCP Server",
|
| 6 |
dependencies=[
|
| 7 |
-
"
|
| 8 |
-
"pydantic-settings>=2.0.0",
|
| 9 |
-
"websockets>=15.0.1",
|
| 10 |
],
|
| 11 |
)
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
@atproto_mcp.tool
|
| 15 |
-
def
|
| 16 |
-
"""
|
| 17 |
try:
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
)
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
except Exception as e:
|
| 26 |
-
return
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
|
| 3 |
+
from atproto import Client
|
| 4 |
+
|
| 5 |
from atproto_mcp.settings import settings
|
| 6 |
from fastmcp import FastMCP
|
| 7 |
|
| 8 |
atproto_mcp = FastMCP(
|
| 9 |
"ATProto MCP Server",
|
| 10 |
dependencies=[
|
| 11 |
+
"atproto_mcp@git+https://github.com/jlowin/fastmcp.git@atproto-example#subdirectory=examples/atproto_mcp",
|
|
|
|
|
|
|
| 12 |
],
|
| 13 |
)
|
| 14 |
|
| 15 |
+
_client: Client | None = None
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def get_client() -> Client:
|
| 19 |
+
"""Get or create an authenticated ATProto client."""
|
| 20 |
+
global _client
|
| 21 |
+
if _client is None:
|
| 22 |
+
_client = Client()
|
| 23 |
+
_client.login(settings.atproto_handle, settings.atproto_password)
|
| 24 |
+
return _client
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@atproto_mcp.tool
|
| 28 |
+
def atproto_status() -> dict:
|
| 29 |
+
"""Check the status of the ATProto connection and current user."""
|
| 30 |
+
try:
|
| 31 |
+
client = get_client()
|
| 32 |
+
profile = client.get_profile(client.me.did)
|
| 33 |
+
return {
|
| 34 |
+
"connected": True,
|
| 35 |
+
"handle": profile.handle,
|
| 36 |
+
"display_name": profile.display_name,
|
| 37 |
+
"did": client.me.did,
|
| 38 |
+
"followers": profile.followers_count,
|
| 39 |
+
"following": profile.follows_count,
|
| 40 |
+
"posts": profile.posts_count,
|
| 41 |
+
}
|
| 42 |
+
except Exception as e:
|
| 43 |
+
return {"connected": False, "error": str(e)}
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
@atproto_mcp.tool
|
| 47 |
+
def post_to_bluesky(text: str) -> dict:
|
| 48 |
+
"""Create a new post on Bluesky."""
|
| 49 |
+
try:
|
| 50 |
+
client = get_client()
|
| 51 |
+
post = client.send_post(text=text)
|
| 52 |
+
return {
|
| 53 |
+
"success": True,
|
| 54 |
+
"uri": post.uri,
|
| 55 |
+
"cid": post.cid,
|
| 56 |
+
"text": text,
|
| 57 |
+
"created_at": datetime.now().isoformat(),
|
| 58 |
+
}
|
| 59 |
+
except Exception as e:
|
| 60 |
+
return {"success": False, "error": str(e)}
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
@atproto_mcp.tool
|
| 64 |
+
def get_timeline(limit: int = 10) -> dict:
|
| 65 |
+
"""Get the authenticated user's timeline."""
|
| 66 |
+
try:
|
| 67 |
+
client = get_client()
|
| 68 |
+
timeline = client.get_timeline(limit=limit)
|
| 69 |
+
|
| 70 |
+
posts = []
|
| 71 |
+
for feed_view in timeline.feed:
|
| 72 |
+
post = feed_view.post
|
| 73 |
+
posts.append(
|
| 74 |
+
{
|
| 75 |
+
"author": post.author.handle,
|
| 76 |
+
"text": post.record.text if hasattr(post.record, "text") else None,
|
| 77 |
+
"created_at": post.record.created_at
|
| 78 |
+
if hasattr(post.record, "created_at")
|
| 79 |
+
else None,
|
| 80 |
+
"likes": post.like_count,
|
| 81 |
+
"reposts": post.repost_count,
|
| 82 |
+
"replies": post.reply_count,
|
| 83 |
+
"uri": post.uri,
|
| 84 |
+
}
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
return {
|
| 88 |
+
"success": True,
|
| 89 |
+
"count": len(posts),
|
| 90 |
+
"posts": posts,
|
| 91 |
+
}
|
| 92 |
+
except Exception as e:
|
| 93 |
+
return {"success": False, "error": str(e)}
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
@atproto_mcp.tool
|
| 97 |
+
def search_posts(query: str, limit: int = 10) -> dict:
|
| 98 |
+
"""Search for posts containing specific text."""
|
| 99 |
+
try:
|
| 100 |
+
client = get_client()
|
| 101 |
+
search_results = client.app.bsky.feed.search_posts(
|
| 102 |
+
q=query,
|
| 103 |
+
limit=limit,
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
posts = []
|
| 107 |
+
for post in search_results.posts:
|
| 108 |
+
posts.append(
|
| 109 |
+
{
|
| 110 |
+
"author": post.author.handle,
|
| 111 |
+
"text": post.record.text if hasattr(post.record, "text") else None,
|
| 112 |
+
"created_at": post.record.created_at
|
| 113 |
+
if hasattr(post.record, "created_at")
|
| 114 |
+
else None,
|
| 115 |
+
"likes": post.like_count,
|
| 116 |
+
"reposts": post.repost_count,
|
| 117 |
+
"replies": post.reply_count,
|
| 118 |
+
"uri": post.uri,
|
| 119 |
+
}
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
return {
|
| 123 |
+
"success": True,
|
| 124 |
+
"query": query,
|
| 125 |
+
"count": len(posts),
|
| 126 |
+
"posts": posts,
|
| 127 |
+
}
|
| 128 |
+
except Exception as e:
|
| 129 |
+
return {"success": False, "error": str(e)}
|
| 130 |
+
|
| 131 |
|
| 132 |
@atproto_mcp.tool
|
| 133 |
+
def get_notifications(limit: int = 10) -> dict:
|
| 134 |
+
"""Get recent notifications for the authenticated user."""
|
| 135 |
try:
|
| 136 |
+
client = get_client()
|
| 137 |
+
notifications = client.app.bsky.notification.list_notifications(limit=limit)
|
| 138 |
+
|
| 139 |
+
notifs = []
|
| 140 |
+
for notif in notifications.notifications:
|
| 141 |
+
notifs.append(
|
| 142 |
+
{
|
| 143 |
+
"reason": notif.reason,
|
| 144 |
+
"author": notif.author.handle if notif.author else None,
|
| 145 |
+
"is_read": notif.is_read,
|
| 146 |
+
"created_at": notif.indexed_at,
|
| 147 |
+
"uri": notif.uri,
|
| 148 |
+
}
|
| 149 |
)
|
| 150 |
+
|
| 151 |
+
return {
|
| 152 |
+
"success": True,
|
| 153 |
+
"count": len(notifs),
|
| 154 |
+
"notifications": notifs,
|
| 155 |
+
}
|
| 156 |
+
except Exception as e:
|
| 157 |
+
return {"success": False, "error": str(e)}
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
@atproto_mcp.tool
|
| 161 |
+
def follow_user(handle: str) -> dict:
|
| 162 |
+
"""Follow a user by their handle."""
|
| 163 |
+
try:
|
| 164 |
+
client = get_client()
|
| 165 |
+
# Resolve handle to DID
|
| 166 |
+
resolved = client.app.bsky.actor.search_actors(q=handle, limit=1)
|
| 167 |
+
if not resolved.actors:
|
| 168 |
+
return {"success": False, "error": f"User {handle} not found"}
|
| 169 |
+
|
| 170 |
+
user_did = resolved.actors[0].did
|
| 171 |
+
follow = client.follow(user_did)
|
| 172 |
+
|
| 173 |
+
return {
|
| 174 |
+
"success": True,
|
| 175 |
+
"followed": handle,
|
| 176 |
+
"did": user_did,
|
| 177 |
+
"uri": follow.uri,
|
| 178 |
+
}
|
| 179 |
+
except Exception as e:
|
| 180 |
+
return {"success": False, "error": str(e)}
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
@atproto_mcp.tool
|
| 184 |
+
def like_post(uri: str) -> dict:
|
| 185 |
+
"""Like a post by its AT URI."""
|
| 186 |
+
try:
|
| 187 |
+
client = get_client()
|
| 188 |
+
like = client.like(uri)
|
| 189 |
+
return {
|
| 190 |
+
"success": True,
|
| 191 |
+
"liked_uri": uri,
|
| 192 |
+
"like_uri": like.uri,
|
| 193 |
+
}
|
| 194 |
+
except Exception as e:
|
| 195 |
+
return {"success": False, "error": str(e)}
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
@atproto_mcp.tool
|
| 199 |
+
def repost(uri: str) -> dict:
|
| 200 |
+
"""Repost a post by its AT URI."""
|
| 201 |
+
try:
|
| 202 |
+
client = get_client()
|
| 203 |
+
repost = client.repost(uri)
|
| 204 |
+
return {
|
| 205 |
+
"success": True,
|
| 206 |
+
"reposted_uri": uri,
|
| 207 |
+
"repost_uri": repost.uri,
|
| 208 |
+
}
|
| 209 |
except Exception as e:
|
| 210 |
+
return {"success": False, "error": str(e)}
|