Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ bayesian*
__pycache__/
.env
config/user.json
.claude/pollinations-user.json
social-preview*
repository-open-graph*
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ This plugin uses Pollinations' [Bring Your Own Pollen](https://github.com/pollin

1. User runs `/pollinations-setup`
2. Browser opens to Pollinations login
3. User authorizes the app
4. API key is captured automatically via localhost redirect
3. User approves the displayed device authorization
4. The plugin polls Pollinations until authorization completes
5. Key is saved locally — never transmitted anywhere except Pollinations API endpoints
6. User's own pollen credits fund all requests

Expand Down
4 changes: 2 additions & 2 deletions commands/pollinations-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

Connect your Pollinations account to Claude Code using BYOP (Bring Your Own Pollen).

This opens your browser to log in at Pollinations. Once authorized, your API key is saved locally and all generation tools become available.
This opens Pollinations' device authorization page in your browser. Approve the displayed code to save your delegated API key locally and enable the generation tools.

Your pollen credits fund usage — the plugin costs nothing to run.

## Usage

Just type `/pollinations-setup` and follow the browser prompt.
Just type `/pollinations-setup` and approve the device authorization in your browser.

If you already have an API key, you can pass it directly:
```
Expand Down
172 changes: 56 additions & 116 deletions servers/pollinations_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
Developer pays nothing. User's pollen credits fund requests.

Base URL: https://gen.pollinations.ai
Image URL: https://image.pollinations.ai
Auth: Bearer token from BYOP OAuth flow
Auth: Bearer token from BYOP OAuth device flow
"""
import json
import os
Expand Down Expand Up @@ -36,7 +35,8 @@
USER_CONFIG_FILE = PROJECT_DIR / ".claude" / "pollinations-user.json"
IMAGE_URL = "https://gen.pollinations.ai/image" # GET /image/{prompt} for images and video
BASE_URL = "https://gen.pollinations.ai"
BYOP_AUTH_URL = "https://enter.pollinations.ai/authorize"
DEVICE_CODE_URL = "https://enter.pollinations.ai/api/device/code"
DEVICE_TOKEN_URL = "https://enter.pollinations.ai/api/device/token"
API_KEY = "" # Key comes from user.json via BYOP OAuth, not env vars

# Developer app key — registered at enter.pollinations.ai as ai-ministries.
Expand Down Expand Up @@ -75,6 +75,7 @@ def _save_user_config(data):
existing.update(data)
USER_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
USER_CONFIG_FILE.write_text(json.dumps(existing, indent=2), encoding="utf-8")
USER_CONFIG_FILE.chmod(0o600)


def _load_styles():
Expand Down Expand Up @@ -151,7 +152,7 @@ def _require_key(config=None):

@mcp.tool()
def pollinations_setup(api_key: str = "") -> str:
"""Connect to Pollinations via BYOP. Opens browser for login, catches the key automatically. If user already has a key they can pass it directly.
"""Connect to Pollinations via BYOP device authorization. If the user already has a key they can pass it directly.

Args:
api_key: Optional — pass a key directly if user has one. Otherwise leave empty to start BYOP login flow.
Expand All @@ -160,120 +161,59 @@ def pollinations_setup(api_key: str = "") -> str:
_save_user_config({"api_key": api_key})
return f"Connected. Key saved. Ready to generate."

# BYOP OAuth flow — spin up localhost server, open browser, catch redirect
import threading
import http.server
# OAuth device flow is designed for CLIs and MCP servers. It avoids putting
# access tokens in URL fragments or running a local callback server.
import webbrowser

captured_key = [None]
server_ready = threading.Event()

class CallbackHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
# Serve a page that extracts the key from the URL fragment
# Fragment (#api_key=...) never hits the server, so we use JS to POST it back
if self.path.startswith("/capture"):
# JS posted the key to us
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length).decode("utf-8") if length else ""
try:
data = json.loads(body)
captured_key[0] = data.get("api_key", "")
except Exception:
pass
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
self.wfile.write(b"<html><body><h2>Connected! You can close this tab.</h2></body></html>")
return

if self.path.startswith("/options") or self.path == "/capture":
self.send_response(200)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.end_headers()
return

# Main callback page — extracts fragment and POSTs key back to /capture
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
page = """<html><body>
<h2>Connecting to Pollinations...</h2>
<script>
var key = new URLSearchParams(window.location.hash.slice(1)).get('api_key');
if (key) {
fetch('/capture', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({api_key:key})})
.then(function(){document.body.innerHTML='<h2>Connected! You can close this tab.</h2>';});
} else {
document.body.innerHTML='<h2>No key received. Try again.</h2>';
}
</script>
</body></html>"""
self.wfile.write(page.encode("utf-8"))

def do_POST(self):
self.do_GET()

def do_OPTIONS(self):
self.send_response(200)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.end_headers()

def log_message(self, format, *args):
pass # silence logs

# Find a free port
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
sock.close()

redirect_url = f"http://localhost:{port}"

# Build auth URL
app_key = APP_KEY or _load_config().get("app_key", "")
params = {"redirect_url": redirect_url}
if app_key:
params["app_key"] = app_key
auth_url = f"{BYOP_AUTH_URL}?{urllib.parse.urlencode(params)}"

# Start server in background
server = http.server.HTTPServer(("127.0.0.1", port), CallbackHandler)
server.timeout = 120

def serve():
server_ready.set()
for _ in range(60): # wait up to 2 min
server.handle_request()
if captured_key[0]:
break

t = threading.Thread(target=serve, daemon=True)
t.start()
server_ready.wait()

# Open browser
webbrowser.open(auth_url)

# Wait for key (up to 2 min)
t.join(timeout=120)

try:
server.server_close()
except Exception:
pass

if captured_key[0]:
_save_user_config({"api_key": captured_key[0]})
return f"Connected! Key saved. Ready to generate."
else:
return "Timed out waiting for login. Run /pollinations-setup to try again."
if not app_key:
return "No Pollinations app key configured. Set POLLINATIONS_APP_KEY and try again."

status, device = _http_json(
DEVICE_CODE_URL,
method="POST",
data={"client_id": app_key},
headers={"Content-Type": "application/json"},
)
if status != 200 or not device.get("device_code"):
return f"Could not start Pollinations login (HTTP {status}): {device.get('error', 'unknown error')}"

verification_uri = device.get("verification_uri", "/device")
if verification_uri.startswith("/"):
verification_uri = f"https://enter.pollinations.ai{verification_uri}"
verification_url = device.get("verification_uri_complete") or verification_uri
user_code = device.get("user_code", "")
if not device.get("verification_uri_complete") and user_code:
separator = "&" if "?" in verification_url else "?"
verification_url = f"{verification_url}{separator}user_code={urllib.parse.quote(user_code)}"

webbrowser.open(verification_url)

interval = max(int(device.get("interval", 5)), 1)
expires_at = time.monotonic() + int(device.get("expires_in", 600))
while time.monotonic() < expires_at:
time.sleep(interval)
status, token = _http_json(
DEVICE_TOKEN_URL,
method="POST",
data={
"device_code": device["device_code"],
"client_id": app_key,
},
headers={"Content-Type": "application/json"},
)
if status == 200 and token.get("access_token"):
_save_user_config({"api_key": token["access_token"]})
return "Connected! Key saved. Ready to generate."

error = token.get("error")
if error == "authorization_pending":
continue
if error == "slow_down":
interval += 5
continue
return f"Pollinations login failed: {error or f'HTTP {status}'}"

return "Pollinations login expired. Run /pollinations-setup to try again."


@mcp.tool()
Expand Down