diff --git a/.gitignore b/.gitignore index c79564c..d2a4aef 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ bayesian* __pycache__/ .env config/user.json +.claude/pollinations-user.json social-preview* repository-open-graph* diff --git a/README.md b/README.md index 5f24523..1ef272d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/commands/pollinations-setup.md b/commands/pollinations-setup.md index ecac266..89576d7 100644 --- a/commands/pollinations-setup.md +++ b/commands/pollinations-setup.md @@ -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: ``` diff --git a/servers/pollinations_server.py b/servers/pollinations_server.py index e56447f..93a1ef5 100644 --- a/servers/pollinations_server.py +++ b/servers/pollinations_server.py @@ -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 @@ -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. @@ -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(): @@ -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. @@ -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"