diff --git a/.agents/skills/agent-browser/SKILL.md b/.agents/skills/agent-browser/SKILL.md new file mode 100644 index 00000000..5cd897ad --- /dev/null +++ b/.agents/skills/agent-browser/SKILL.md @@ -0,0 +1,217 @@ +--- +name: agent-browser +description: Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction. +allowed-tools: Bash(agent-browser:*) +--- + +# Browser Automation with agent-browser + +## Core Workflow + +Every browser automation follows this pattern: + +1. **Navigate**: `agent-browser open ` +2. **Snapshot**: `agent-browser snapshot -i` (get element refs like `@e1`, `@e2`) +3. **Interact**: Use refs to click, fill, select +4. **Re-snapshot**: After navigation or DOM changes, get fresh refs + +```bash +agent-browser open https://example.com/form +agent-browser snapshot -i +# Output: @e1 [input type="email"], @e2 [input type="password"], @e3 [button] "Submit" + +agent-browser fill @e1 "user@example.com" +agent-browser fill @e2 "password123" +agent-browser click @e3 +agent-browser wait --load networkidle +agent-browser snapshot -i # Check result +``` + +## Essential Commands + +```bash +# Navigation +agent-browser open # Navigate (aliases: goto, navigate) +agent-browser close # Close browser + +# Snapshot +agent-browser snapshot -i # Interactive elements with refs (recommended) +agent-browser snapshot -i -C # Include cursor-interactive elements (divs with onclick, cursor:pointer) +agent-browser snapshot -s "#selector" # Scope to CSS selector + +# Interaction (use @refs from snapshot) +agent-browser click @e1 # Click element +agent-browser fill @e2 "text" # Clear and type text +agent-browser type @e2 "text" # Type without clearing +agent-browser select @e1 "option" # Select dropdown option +agent-browser check @e1 # Check checkbox +agent-browser press Enter # Press key +agent-browser scroll down 500 # Scroll page + +# Get information +agent-browser get text @e1 # Get element text +agent-browser get url # Get current URL +agent-browser get title # Get page title + +# Wait +agent-browser wait @e1 # Wait for element +agent-browser wait --load networkidle # Wait for network idle +agent-browser wait --url "**/page" # Wait for URL pattern +agent-browser wait 2000 # Wait milliseconds + +# Capture +agent-browser screenshot # Screenshot to temp dir +agent-browser screenshot --full # Full page screenshot +agent-browser pdf output.pdf # Save as PDF +``` + +## Common Patterns + +### Form Submission + +```bash +agent-browser open https://example.com/signup +agent-browser snapshot -i +agent-browser fill @e1 "Jane Doe" +agent-browser fill @e2 "jane@example.com" +agent-browser select @e3 "California" +agent-browser check @e4 +agent-browser click @e5 +agent-browser wait --load networkidle +``` + +### Authentication with State Persistence + +```bash +# Login once and save state +agent-browser open https://app.example.com/login +agent-browser snapshot -i +agent-browser fill @e1 "$USERNAME" +agent-browser fill @e2 "$PASSWORD" +agent-browser click @e3 +agent-browser wait --url "**/dashboard" +agent-browser state save auth.json + +# Reuse in future sessions +agent-browser state load auth.json +agent-browser open https://app.example.com/dashboard +``` + +### Data Extraction + +```bash +agent-browser open https://example.com/products +agent-browser snapshot -i +agent-browser get text @e5 # Get specific element text +agent-browser get text body > page.txt # Get all page text + +# JSON output for parsing +agent-browser snapshot -i --json +agent-browser get text @e1 --json +``` + +### Parallel Sessions + +```bash +agent-browser --session site1 open https://site-a.com +agent-browser --session site2 open https://site-b.com + +agent-browser --session site1 snapshot -i +agent-browser --session site2 snapshot -i + +agent-browser session list +``` + +### Visual Browser (Debugging) + +```bash +agent-browser --headed open https://example.com +agent-browser highlight @e1 # Highlight element +agent-browser record start demo.webm # Record session +``` + +### Local Files (PDFs, HTML) + +```bash +# Open local files with file:// URLs +agent-browser --allow-file-access open file:///path/to/document.pdf +agent-browser --allow-file-access open file:///path/to/page.html +agent-browser screenshot output.png +``` + +### iOS Simulator (Mobile Safari) + +```bash +# List available iOS simulators +agent-browser device list + +# Launch Safari on a specific device +agent-browser -p ios --device "iPhone 16 Pro" open https://example.com + +# Same workflow as desktop - snapshot, interact, re-snapshot +agent-browser -p ios snapshot -i +agent-browser -p ios tap @e1 # Tap (alias for click) +agent-browser -p ios fill @e2 "text" +agent-browser -p ios swipe up # Mobile-specific gesture + +# Take screenshot +agent-browser -p ios screenshot mobile.png + +# Close session (shuts down simulator) +agent-browser -p ios close +``` + +**Requirements:** macOS with Xcode, Appium (`npm install -g appium && appium driver install xcuitest`) + +**Real devices:** Works with physical iOS devices if pre-configured. Use `--device ""` where UDID is from `xcrun xctrace list devices`. + +## Ref Lifecycle (Important) + +Refs (`@e1`, `@e2`, etc.) are invalidated when the page changes. Always re-snapshot after: + +- Clicking links or buttons that navigate +- Form submissions +- Dynamic content loading (dropdowns, modals) + +```bash +agent-browser click @e5 # Navigates to new page +agent-browser snapshot -i # MUST re-snapshot +agent-browser click @e1 # Use new refs +``` + +## Semantic Locators (Alternative to Refs) + +When refs are unavailable or unreliable, use semantic locators: + +```bash +agent-browser find text "Sign In" click +agent-browser find label "Email" fill "user@test.com" +agent-browser find role button click --name "Submit" +agent-browser find placeholder "Search" type "query" +agent-browser find testid "submit-btn" click +``` + +## Deep-Dive Documentation + +| Reference | When to Use | +|-----------|-------------| +| [references/commands.md](references/commands.md) | Full command reference with all options | +| [references/snapshot-refs.md](references/snapshot-refs.md) | Ref lifecycle, invalidation rules, troubleshooting | +| [references/session-management.md](references/session-management.md) | Parallel sessions, state persistence, concurrent scraping | +| [references/authentication.md](references/authentication.md) | Login flows, OAuth, 2FA handling, state reuse | +| [references/video-recording.md](references/video-recording.md) | Recording workflows for debugging and documentation | +| [references/proxy-support.md](references/proxy-support.md) | Proxy configuration, geo-testing, rotating proxies | + +## Ready-to-Use Templates + +| Template | Description | +|----------|-------------| +| [templates/form-automation.sh](templates/form-automation.sh) | Form filling with validation | +| [templates/authenticated-session.sh](templates/authenticated-session.sh) | Login once, reuse state | +| [templates/capture-workflow.sh](templates/capture-workflow.sh) | Content extraction with screenshots | + +```bash +./templates/form-automation.sh https://example.com/form +./templates/authenticated-session.sh https://app.example.com/login +./templates/capture-workflow.sh https://example.com ./output +``` diff --git a/.agents/skills/agent-browser/references/authentication.md b/.agents/skills/agent-browser/references/authentication.md new file mode 100644 index 00000000..12ef5e41 --- /dev/null +++ b/.agents/skills/agent-browser/references/authentication.md @@ -0,0 +1,202 @@ +# Authentication Patterns + +Login flows, session persistence, OAuth, 2FA, and authenticated browsing. + +**Related**: [session-management.md](session-management.md) for state persistence details, [SKILL.md](../SKILL.md) for quick start. + +## Contents + +- [Basic Login Flow](#basic-login-flow) +- [Saving Authentication State](#saving-authentication-state) +- [Restoring Authentication](#restoring-authentication) +- [OAuth / SSO Flows](#oauth--sso-flows) +- [Two-Factor Authentication](#two-factor-authentication) +- [HTTP Basic Auth](#http-basic-auth) +- [Cookie-Based Auth](#cookie-based-auth) +- [Token Refresh Handling](#token-refresh-handling) +- [Security Best Practices](#security-best-practices) + +## Basic Login Flow + +```bash +# Navigate to login page +agent-browser open https://app.example.com/login +agent-browser wait --load networkidle + +# Get form elements +agent-browser snapshot -i +# Output: @e1 [input type="email"], @e2 [input type="password"], @e3 [button] "Sign In" + +# Fill credentials +agent-browser fill @e1 "user@example.com" +agent-browser fill @e2 "password123" + +# Submit +agent-browser click @e3 +agent-browser wait --load networkidle + +# Verify login succeeded +agent-browser get url # Should be dashboard, not login +``` + +## Saving Authentication State + +After logging in, save state for reuse: + +```bash +# Login first (see above) +agent-browser open https://app.example.com/login +agent-browser snapshot -i +agent-browser fill @e1 "user@example.com" +agent-browser fill @e2 "password123" +agent-browser click @e3 +agent-browser wait --url "**/dashboard" + +# Save authenticated state +agent-browser state save ./auth-state.json +``` + +## Restoring Authentication + +Skip login by loading saved state: + +```bash +# Load saved auth state +agent-browser state load ./auth-state.json + +# Navigate directly to protected page +agent-browser open https://app.example.com/dashboard + +# Verify authenticated +agent-browser snapshot -i +``` + +## OAuth / SSO Flows + +For OAuth redirects: + +```bash +# Start OAuth flow +agent-browser open https://app.example.com/auth/google + +# Handle redirects automatically +agent-browser wait --url "**/accounts.google.com**" +agent-browser snapshot -i + +# Fill Google credentials +agent-browser fill @e1 "user@gmail.com" +agent-browser click @e2 # Next button +agent-browser wait 2000 +agent-browser snapshot -i +agent-browser fill @e3 "password" +agent-browser click @e4 # Sign in + +# Wait for redirect back +agent-browser wait --url "**/app.example.com**" +agent-browser state save ./oauth-state.json +``` + +## Two-Factor Authentication + +Handle 2FA with manual intervention: + +```bash +# Login with credentials +agent-browser open https://app.example.com/login --headed # Show browser +agent-browser snapshot -i +agent-browser fill @e1 "user@example.com" +agent-browser fill @e2 "password123" +agent-browser click @e3 + +# Wait for user to complete 2FA manually +echo "Complete 2FA in the browser window..." +agent-browser wait --url "**/dashboard" --timeout 120000 + +# Save state after 2FA +agent-browser state save ./2fa-state.json +``` + +## HTTP Basic Auth + +For sites using HTTP Basic Authentication: + +```bash +# Set credentials before navigation +agent-browser set credentials username password + +# Navigate to protected resource +agent-browser open https://protected.example.com/api +``` + +## Cookie-Based Auth + +Manually set authentication cookies: + +```bash +# Set auth cookie +agent-browser cookies set session_token "abc123xyz" + +# Navigate to protected page +agent-browser open https://app.example.com/dashboard +``` + +## Token Refresh Handling + +For sessions with expiring tokens: + +```bash +#!/bin/bash +# Wrapper that handles token refresh + +STATE_FILE="./auth-state.json" + +# Try loading existing state +if [[ -f "$STATE_FILE" ]]; then + agent-browser state load "$STATE_FILE" + agent-browser open https://app.example.com/dashboard + + # Check if session is still valid + URL=$(agent-browser get url) + if [[ "$URL" == *"/login"* ]]; then + echo "Session expired, re-authenticating..." + # Perform fresh login + agent-browser snapshot -i + agent-browser fill @e1 "$USERNAME" + agent-browser fill @e2 "$PASSWORD" + agent-browser click @e3 + agent-browser wait --url "**/dashboard" + agent-browser state save "$STATE_FILE" + fi +else + # First-time login + agent-browser open https://app.example.com/login + # ... login flow ... +fi +``` + +## Security Best Practices + +1. **Never commit state files** - They contain session tokens + ```bash + echo "*.auth-state.json" >> .gitignore + ``` + +2. **Use environment variables for credentials** + ```bash + agent-browser fill @e1 "$APP_USERNAME" + agent-browser fill @e2 "$APP_PASSWORD" + ``` + +3. **Clean up after automation** + ```bash + agent-browser cookies clear + rm -f ./auth-state.json + ``` + +4. **Use short-lived sessions for CI/CD** + ```bash + # Don't persist state in CI + agent-browser open https://app.example.com/login + # ... login and perform actions ... + agent-browser close # Session ends, nothing persisted + ``` diff --git a/.agents/skills/agent-browser/references/commands.md b/.agents/skills/agent-browser/references/commands.md new file mode 100644 index 00000000..8744accf --- /dev/null +++ b/.agents/skills/agent-browser/references/commands.md @@ -0,0 +1,259 @@ +# Command Reference + +Complete reference for all agent-browser commands. For quick start and common patterns, see SKILL.md. + +## Navigation + +```bash +agent-browser open # Navigate to URL (aliases: goto, navigate) + # Supports: https://, http://, file://, about:, data:// + # Auto-prepends https:// if no protocol given +agent-browser back # Go back +agent-browser forward # Go forward +agent-browser reload # Reload page +agent-browser close # Close browser (aliases: quit, exit) +agent-browser connect 9222 # Connect to browser via CDP port +``` + +## Snapshot (page analysis) + +```bash +agent-browser snapshot # Full accessibility tree +agent-browser snapshot -i # Interactive elements only (recommended) +agent-browser snapshot -c # Compact output +agent-browser snapshot -d 3 # Limit depth to 3 +agent-browser snapshot -s "#main" # Scope to CSS selector +``` + +## Interactions (use @refs from snapshot) + +```bash +agent-browser click @e1 # Click +agent-browser dblclick @e1 # Double-click +agent-browser focus @e1 # Focus element +agent-browser fill @e2 "text" # Clear and type +agent-browser type @e2 "text" # Type without clearing +agent-browser press Enter # Press key (alias: key) +agent-browser press Control+a # Key combination +agent-browser keydown Shift # Hold key down +agent-browser keyup Shift # Release key +agent-browser hover @e1 # Hover +agent-browser check @e1 # Check checkbox +agent-browser uncheck @e1 # Uncheck checkbox +agent-browser select @e1 "value" # Select dropdown option +agent-browser select @e1 "a" "b" # Select multiple options +agent-browser scroll down 500 # Scroll page (default: down 300px) +agent-browser scrollintoview @e1 # Scroll element into view (alias: scrollinto) +agent-browser drag @e1 @e2 # Drag and drop +agent-browser upload @e1 file.pdf # Upload files +``` + +## Get Information + +```bash +agent-browser get text @e1 # Get element text +agent-browser get html @e1 # Get innerHTML +agent-browser get value @e1 # Get input value +agent-browser get attr @e1 href # Get attribute +agent-browser get title # Get page title +agent-browser get url # Get current URL +agent-browser get count ".item" # Count matching elements +agent-browser get box @e1 # Get bounding box +agent-browser get styles @e1 # Get computed styles (font, color, bg, etc.) +``` + +## Check State + +```bash +agent-browser is visible @e1 # Check if visible +agent-browser is enabled @e1 # Check if enabled +agent-browser is checked @e1 # Check if checked +``` + +## Screenshots and PDF + +```bash +agent-browser screenshot # Save to temporary directory +agent-browser screenshot path.png # Save to specific path +agent-browser screenshot --full # Full page +agent-browser pdf output.pdf # Save as PDF +``` + +## Video Recording + +```bash +agent-browser record start ./demo.webm # Start recording +agent-browser click @e1 # Perform actions +agent-browser record stop # Stop and save video +agent-browser record restart ./take2.webm # Stop current + start new +``` + +## Wait + +```bash +agent-browser wait @e1 # Wait for element +agent-browser wait 2000 # Wait milliseconds +agent-browser wait --text "Success" # Wait for text (or -t) +agent-browser wait --url "**/dashboard" # Wait for URL pattern (or -u) +agent-browser wait --load networkidle # Wait for network idle (or -l) +agent-browser wait --fn "window.ready" # Wait for JS condition (or -f) +``` + +## Mouse Control + +```bash +agent-browser mouse move 100 200 # Move mouse +agent-browser mouse down left # Press button +agent-browser mouse up left # Release button +agent-browser mouse wheel 100 # Scroll wheel +``` + +## Semantic Locators (alternative to refs) + +```bash +agent-browser find role button click --name "Submit" +agent-browser find text "Sign In" click +agent-browser find text "Sign In" click --exact # Exact match only +agent-browser find label "Email" fill "user@test.com" +agent-browser find placeholder "Search" type "query" +agent-browser find alt "Logo" click +agent-browser find title "Close" click +agent-browser find testid "submit-btn" click +agent-browser find first ".item" click +agent-browser find last ".item" click +agent-browser find nth 2 "a" hover +``` + +## Browser Settings + +```bash +agent-browser set viewport 1920 1080 # Set viewport size +agent-browser set device "iPhone 14" # Emulate device +agent-browser set geo 37.7749 -122.4194 # Set geolocation (alias: geolocation) +agent-browser set offline on # Toggle offline mode +agent-browser set headers '{"X-Key":"v"}' # Extra HTTP headers +agent-browser set credentials user pass # HTTP basic auth (alias: auth) +agent-browser set media dark # Emulate color scheme +agent-browser set media light reduced-motion # Light mode + reduced motion +``` + +## Cookies and Storage + +```bash +agent-browser cookies # Get all cookies +agent-browser cookies set name value # Set cookie +agent-browser cookies clear # Clear cookies +agent-browser storage local # Get all localStorage +agent-browser storage local key # Get specific key +agent-browser storage local set k v # Set value +agent-browser storage local clear # Clear all +``` + +## Network + +```bash +agent-browser network route # Intercept requests +agent-browser network route --abort # Block requests +agent-browser network route --body '{}' # Mock response +agent-browser network unroute [url] # Remove routes +agent-browser network requests # View tracked requests +agent-browser network requests --filter api # Filter requests +``` + +## Tabs and Windows + +```bash +agent-browser tab # List tabs +agent-browser tab new [url] # New tab +agent-browser tab 2 # Switch to tab by index +agent-browser tab close # Close current tab +agent-browser tab close 2 # Close tab by index +agent-browser window new # New window +``` + +## Frames + +```bash +agent-browser frame "#iframe" # Switch to iframe +agent-browser frame main # Back to main frame +``` + +## Dialogs + +```bash +agent-browser dialog accept [text] # Accept dialog +agent-browser dialog dismiss # Dismiss dialog +``` + +## JavaScript + +```bash +agent-browser eval "document.title" # Simple expressions only +agent-browser eval -b "" # Any JavaScript (base64 encoded) +agent-browser eval --stdin # Read script from stdin +``` + +Use `-b`/`--base64` or `--stdin` for reliable execution. Shell escaping with nested quotes and special characters is error-prone. + +```bash +# Base64 encode your script, then: +agent-browser eval -b "ZG9jdW1lbnQucXVlcnlTZWxlY3RvcignW3NyYyo9Il9uZXh0Il0nKQ==" + +# Or use stdin with heredoc for multiline scripts: +cat <<'EOF' | agent-browser eval --stdin +const links = document.querySelectorAll('a'); +Array.from(links).map(a => a.href); +EOF +``` + +## State Management + +```bash +agent-browser state save auth.json # Save cookies, storage, auth state +agent-browser state load auth.json # Restore saved state +``` + +## Global Options + +```bash +agent-browser --session ... # Isolated browser session +agent-browser --json ... # JSON output for parsing +agent-browser --headed ... # Show browser window (not headless) +agent-browser --full ... # Full page screenshot (-f) +agent-browser --cdp ... # Connect via Chrome DevTools Protocol +agent-browser -p ... # Cloud browser provider (--provider) +agent-browser --proxy ... # Use proxy server +agent-browser --headers ... # HTTP headers scoped to URL's origin +agent-browser --executable-path

# Custom browser executable +agent-browser --extension ... # Load browser extension (repeatable) +agent-browser --ignore-https-errors # Ignore SSL certificate errors +agent-browser --help # Show help (-h) +agent-browser --version # Show version (-V) +agent-browser --help # Show detailed help for a command +``` + +## Debugging + +```bash +agent-browser --headed open example.com # Show browser window +agent-browser --cdp 9222 snapshot # Connect via CDP port +agent-browser connect 9222 # Alternative: connect command +agent-browser console # View console messages +agent-browser console --clear # Clear console +agent-browser errors # View page errors +agent-browser errors --clear # Clear errors +agent-browser highlight @e1 # Highlight element +agent-browser trace start # Start recording trace +agent-browser trace stop trace.zip # Stop and save trace +``` + +## Environment Variables + +```bash +AGENT_BROWSER_SESSION="mysession" # Default session name +AGENT_BROWSER_EXECUTABLE_PATH="/path/chrome" # Custom browser path +AGENT_BROWSER_EXTENSIONS="/ext1,/ext2" # Comma-separated extension paths +AGENT_BROWSER_PROVIDER="browserbase" # Cloud browser provider +AGENT_BROWSER_STREAM_PORT="9223" # WebSocket streaming port +AGENT_BROWSER_HOME="/path/to/agent-browser" # Custom install location +``` diff --git a/.agents/skills/agent-browser/references/proxy-support.md b/.agents/skills/agent-browser/references/proxy-support.md new file mode 100644 index 00000000..05cc9d53 --- /dev/null +++ b/.agents/skills/agent-browser/references/proxy-support.md @@ -0,0 +1,188 @@ +# Proxy Support + +Proxy configuration for geo-testing, rate limiting avoidance, and corporate environments. + +**Related**: [commands.md](commands.md) for global options, [SKILL.md](../SKILL.md) for quick start. + +## Contents + +- [Basic Proxy Configuration](#basic-proxy-configuration) +- [Authenticated Proxy](#authenticated-proxy) +- [SOCKS Proxy](#socks-proxy) +- [Proxy Bypass](#proxy-bypass) +- [Common Use Cases](#common-use-cases) +- [Verifying Proxy Connection](#verifying-proxy-connection) +- [Troubleshooting](#troubleshooting) +- [Best Practices](#best-practices) + +## Basic Proxy Configuration + +Set proxy via environment variable before starting: + +```bash +# HTTP proxy +export HTTP_PROXY="http://proxy.example.com:8080" +agent-browser open https://example.com + +# HTTPS proxy +export HTTPS_PROXY="https://proxy.example.com:8080" +agent-browser open https://example.com + +# Both +export HTTP_PROXY="http://proxy.example.com:8080" +export HTTPS_PROXY="http://proxy.example.com:8080" +agent-browser open https://example.com +``` + +## Authenticated Proxy + +For proxies requiring authentication: + +```bash +# Include credentials in URL +export HTTP_PROXY="http://username:password@proxy.example.com:8080" +agent-browser open https://example.com +``` + +## SOCKS Proxy + +```bash +# SOCKS5 proxy +export ALL_PROXY="socks5://proxy.example.com:1080" +agent-browser open https://example.com + +# SOCKS5 with auth +export ALL_PROXY="socks5://user:pass@proxy.example.com:1080" +agent-browser open https://example.com +``` + +## Proxy Bypass + +Skip proxy for specific domains: + +```bash +# Bypass proxy for local addresses +export NO_PROXY="localhost,127.0.0.1,.internal.company.com" +agent-browser open https://internal.company.com # Direct connection +agent-browser open https://external.com # Via proxy +``` + +## Common Use Cases + +### Geo-Location Testing + +```bash +#!/bin/bash +# Test site from different regions using geo-located proxies + +PROXIES=( + "http://us-proxy.example.com:8080" + "http://eu-proxy.example.com:8080" + "http://asia-proxy.example.com:8080" +) + +for proxy in "${PROXIES[@]}"; do + export HTTP_PROXY="$proxy" + export HTTPS_PROXY="$proxy" + + region=$(echo "$proxy" | grep -oP '^\w+-\w+') + echo "Testing from: $region" + + agent-browser --session "$region" open https://example.com + agent-browser --session "$region" screenshot "./screenshots/$region.png" + agent-browser --session "$region" close +done +``` + +### Rotating Proxies for Scraping + +```bash +#!/bin/bash +# Rotate through proxy list to avoid rate limiting + +PROXY_LIST=( + "http://proxy1.example.com:8080" + "http://proxy2.example.com:8080" + "http://proxy3.example.com:8080" +) + +URLS=( + "https://site.com/page1" + "https://site.com/page2" + "https://site.com/page3" +) + +for i in "${!URLS[@]}"; do + proxy_index=$((i % ${#PROXY_LIST[@]})) + export HTTP_PROXY="${PROXY_LIST[$proxy_index]}" + export HTTPS_PROXY="${PROXY_LIST[$proxy_index]}" + + agent-browser open "${URLS[$i]}" + agent-browser get text body > "output-$i.txt" + agent-browser close + + sleep 1 # Polite delay +done +``` + +### Corporate Network Access + +```bash +#!/bin/bash +# Access internal sites via corporate proxy + +export HTTP_PROXY="http://corpproxy.company.com:8080" +export HTTPS_PROXY="http://corpproxy.company.com:8080" +export NO_PROXY="localhost,127.0.0.1,.company.com" + +# External sites go through proxy +agent-browser open https://external-vendor.com + +# Internal sites bypass proxy +agent-browser open https://intranet.company.com +``` + +## Verifying Proxy Connection + +```bash +# Check your apparent IP +agent-browser open https://httpbin.org/ip +agent-browser get text body +# Should show proxy's IP, not your real IP +``` + +## Troubleshooting + +### Proxy Connection Failed + +```bash +# Test proxy connectivity first +curl -x http://proxy.example.com:8080 https://httpbin.org/ip + +# Check if proxy requires auth +export HTTP_PROXY="http://user:pass@proxy.example.com:8080" +``` + +### SSL/TLS Errors Through Proxy + +Some proxies perform SSL inspection. If you encounter certificate errors: + +```bash +# For testing only - not recommended for production +agent-browser open https://example.com --ignore-https-errors +``` + +### Slow Performance + +```bash +# Use proxy only when necessary +export NO_PROXY="*.cdn.com,*.static.com" # Direct CDN access +``` + +## Best Practices + +1. **Use environment variables** - Don't hardcode proxy credentials +2. **Set NO_PROXY appropriately** - Avoid routing local traffic through proxy +3. **Test proxy before automation** - Verify connectivity with simple requests +4. **Handle proxy failures gracefully** - Implement retry logic for unstable proxies +5. **Rotate proxies for large scraping jobs** - Distribute load and avoid bans diff --git a/.agents/skills/agent-browser/references/session-management.md b/.agents/skills/agent-browser/references/session-management.md new file mode 100644 index 00000000..bb5312db --- /dev/null +++ b/.agents/skills/agent-browser/references/session-management.md @@ -0,0 +1,193 @@ +# Session Management + +Multiple isolated browser sessions with state persistence and concurrent browsing. + +**Related**: [authentication.md](authentication.md) for login patterns, [SKILL.md](../SKILL.md) for quick start. + +## Contents + +- [Named Sessions](#named-sessions) +- [Session Isolation Properties](#session-isolation-properties) +- [Session State Persistence](#session-state-persistence) +- [Common Patterns](#common-patterns) +- [Default Session](#default-session) +- [Session Cleanup](#session-cleanup) +- [Best Practices](#best-practices) + +## Named Sessions + +Use `--session` flag to isolate browser contexts: + +```bash +# Session 1: Authentication flow +agent-browser --session auth open https://app.example.com/login + +# Session 2: Public browsing (separate cookies, storage) +agent-browser --session public open https://example.com + +# Commands are isolated by session +agent-browser --session auth fill @e1 "user@example.com" +agent-browser --session public get text body +``` + +## Session Isolation Properties + +Each session has independent: +- Cookies +- LocalStorage / SessionStorage +- IndexedDB +- Cache +- Browsing history +- Open tabs + +## Session State Persistence + +### Save Session State + +```bash +# Save cookies, storage, and auth state +agent-browser state save /path/to/auth-state.json +``` + +### Load Session State + +```bash +# Restore saved state +agent-browser state load /path/to/auth-state.json + +# Continue with authenticated session +agent-browser open https://app.example.com/dashboard +``` + +### State File Contents + +```json +{ + "cookies": [...], + "localStorage": {...}, + "sessionStorage": {...}, + "origins": [...] +} +``` + +## Common Patterns + +### Authenticated Session Reuse + +```bash +#!/bin/bash +# Save login state once, reuse many times + +STATE_FILE="/tmp/auth-state.json" + +# Check if we have saved state +if [[ -f "$STATE_FILE" ]]; then + agent-browser state load "$STATE_FILE" + agent-browser open https://app.example.com/dashboard +else + # Perform login + agent-browser open https://app.example.com/login + agent-browser snapshot -i + agent-browser fill @e1 "$USERNAME" + agent-browser fill @e2 "$PASSWORD" + agent-browser click @e3 + agent-browser wait --load networkidle + + # Save for future use + agent-browser state save "$STATE_FILE" +fi +``` + +### Concurrent Scraping + +```bash +#!/bin/bash +# Scrape multiple sites concurrently + +# Start all sessions +agent-browser --session site1 open https://site1.com & +agent-browser --session site2 open https://site2.com & +agent-browser --session site3 open https://site3.com & +wait + +# Extract from each +agent-browser --session site1 get text body > site1.txt +agent-browser --session site2 get text body > site2.txt +agent-browser --session site3 get text body > site3.txt + +# Cleanup +agent-browser --session site1 close +agent-browser --session site2 close +agent-browser --session site3 close +``` + +### A/B Testing Sessions + +```bash +# Test different user experiences +agent-browser --session variant-a open "https://app.com?variant=a" +agent-browser --session variant-b open "https://app.com?variant=b" + +# Compare +agent-browser --session variant-a screenshot /tmp/variant-a.png +agent-browser --session variant-b screenshot /tmp/variant-b.png +``` + +## Default Session + +When `--session` is omitted, commands use the default session: + +```bash +# These use the same default session +agent-browser open https://example.com +agent-browser snapshot -i +agent-browser close # Closes default session +``` + +## Session Cleanup + +```bash +# Close specific session +agent-browser --session auth close + +# List active sessions +agent-browser session list +``` + +## Best Practices + +### 1. Name Sessions Semantically + +```bash +# GOOD: Clear purpose +agent-browser --session github-auth open https://github.com +agent-browser --session docs-scrape open https://docs.example.com + +# AVOID: Generic names +agent-browser --session s1 open https://github.com +``` + +### 2. Always Clean Up + +```bash +# Close sessions when done +agent-browser --session auth close +agent-browser --session scrape close +``` + +### 3. Handle State Files Securely + +```bash +# Don't commit state files (contain auth tokens!) +echo "*.auth-state.json" >> .gitignore + +# Delete after use +rm /tmp/auth-state.json +``` + +### 4. Timeout Long Sessions + +```bash +# Set timeout for automated scripts +timeout 60 agent-browser --session long-task get text body +``` diff --git a/.agents/skills/agent-browser/references/snapshot-refs.md b/.agents/skills/agent-browser/references/snapshot-refs.md new file mode 100644 index 00000000..c13d53a8 --- /dev/null +++ b/.agents/skills/agent-browser/references/snapshot-refs.md @@ -0,0 +1,194 @@ +# Snapshot and Refs + +Compact element references that reduce context usage dramatically for AI agents. + +**Related**: [commands.md](commands.md) for full command reference, [SKILL.md](../SKILL.md) for quick start. + +## Contents + +- [How Refs Work](#how-refs-work) +- [Snapshot Command](#the-snapshot-command) +- [Using Refs](#using-refs) +- [Ref Lifecycle](#ref-lifecycle) +- [Best Practices](#best-practices) +- [Ref Notation Details](#ref-notation-details) +- [Troubleshooting](#troubleshooting) + +## How Refs Work + +Traditional approach: +``` +Full DOM/HTML → AI parses → CSS selector → Action (~3000-5000 tokens) +``` + +agent-browser approach: +``` +Compact snapshot → @refs assigned → Direct interaction (~200-400 tokens) +``` + +## The Snapshot Command + +```bash +# Basic snapshot (shows page structure) +agent-browser snapshot + +# Interactive snapshot (-i flag) - RECOMMENDED +agent-browser snapshot -i +``` + +### Snapshot Output Format + +``` +Page: Example Site - Home +URL: https://example.com + +@e1 [header] + @e2 [nav] + @e3 [a] "Home" + @e4 [a] "Products" + @e5 [a] "About" + @e6 [button] "Sign In" + +@e7 [main] + @e8 [h1] "Welcome" + @e9 [form] + @e10 [input type="email"] placeholder="Email" + @e11 [input type="password"] placeholder="Password" + @e12 [button type="submit"] "Log In" + +@e13 [footer] + @e14 [a] "Privacy Policy" +``` + +## Using Refs + +Once you have refs, interact directly: + +```bash +# Click the "Sign In" button +agent-browser click @e6 + +# Fill email input +agent-browser fill @e10 "user@example.com" + +# Fill password +agent-browser fill @e11 "password123" + +# Submit the form +agent-browser click @e12 +``` + +## Ref Lifecycle + +**IMPORTANT**: Refs are invalidated when the page changes! + +```bash +# Get initial snapshot +agent-browser snapshot -i +# @e1 [button] "Next" + +# Click triggers page change +agent-browser click @e1 + +# MUST re-snapshot to get new refs! +agent-browser snapshot -i +# @e1 [h1] "Page 2" ← Different element now! +``` + +## Best Practices + +### 1. Always Snapshot Before Interacting + +```bash +# CORRECT +agent-browser open https://example.com +agent-browser snapshot -i # Get refs first +agent-browser click @e1 # Use ref + +# WRONG +agent-browser open https://example.com +agent-browser click @e1 # Ref doesn't exist yet! +``` + +### 2. Re-Snapshot After Navigation + +```bash +agent-browser click @e5 # Navigates to new page +agent-browser snapshot -i # Get new refs +agent-browser click @e1 # Use new refs +``` + +### 3. Re-Snapshot After Dynamic Changes + +```bash +agent-browser click @e1 # Opens dropdown +agent-browser snapshot -i # See dropdown items +agent-browser click @e7 # Select item +``` + +### 4. Snapshot Specific Regions + +For complex pages, snapshot specific areas: + +```bash +# Snapshot just the form +agent-browser snapshot @e9 +``` + +## Ref Notation Details + +``` +@e1 [tag type="value"] "text content" placeholder="hint" +│ │ │ │ │ +│ │ │ │ └─ Additional attributes +│ │ │ └─ Visible text +│ │ └─ Key attributes shown +│ └─ HTML tag name +└─ Unique ref ID +``` + +### Common Patterns + +``` +@e1 [button] "Submit" # Button with text +@e2 [input type="email"] # Email input +@e3 [input type="password"] # Password input +@e4 [a href="/page"] "Link Text" # Anchor link +@e5 [select] # Dropdown +@e6 [textarea] placeholder="Message" # Text area +@e7 [div class="modal"] # Container (when relevant) +@e8 [img alt="Logo"] # Image +@e9 [checkbox] checked # Checked checkbox +@e10 [radio] selected # Selected radio +``` + +## Troubleshooting + +### "Ref not found" Error + +```bash +# Ref may have changed - re-snapshot +agent-browser snapshot -i +``` + +### Element Not Visible in Snapshot + +```bash +# Scroll to reveal element +agent-browser scroll --bottom +agent-browser snapshot -i + +# Or wait for dynamic content +agent-browser wait 1000 +agent-browser snapshot -i +``` + +### Too Many Elements + +```bash +# Snapshot specific container +agent-browser snapshot @e5 + +# Or use get text for content-only extraction +agent-browser get text @e5 +``` diff --git a/.agents/skills/agent-browser/references/video-recording.md b/.agents/skills/agent-browser/references/video-recording.md new file mode 100644 index 00000000..e6a9fb4e --- /dev/null +++ b/.agents/skills/agent-browser/references/video-recording.md @@ -0,0 +1,173 @@ +# Video Recording + +Capture browser automation as video for debugging, documentation, or verification. + +**Related**: [commands.md](commands.md) for full command reference, [SKILL.md](../SKILL.md) for quick start. + +## Contents + +- [Basic Recording](#basic-recording) +- [Recording Commands](#recording-commands) +- [Use Cases](#use-cases) +- [Best Practices](#best-practices) +- [Output Format](#output-format) +- [Limitations](#limitations) + +## Basic Recording + +```bash +# Start recording +agent-browser record start ./demo.webm + +# Perform actions +agent-browser open https://example.com +agent-browser snapshot -i +agent-browser click @e1 +agent-browser fill @e2 "test input" + +# Stop and save +agent-browser record stop +``` + +## Recording Commands + +```bash +# Start recording to file +agent-browser record start ./output.webm + +# Stop current recording +agent-browser record stop + +# Restart with new file (stops current + starts new) +agent-browser record restart ./take2.webm +``` + +## Use Cases + +### Debugging Failed Automation + +```bash +#!/bin/bash +# Record automation for debugging + +agent-browser record start ./debug-$(date +%Y%m%d-%H%M%S).webm + +# Run your automation +agent-browser open https://app.example.com +agent-browser snapshot -i +agent-browser click @e1 || { + echo "Click failed - check recording" + agent-browser record stop + exit 1 +} + +agent-browser record stop +``` + +### Documentation Generation + +```bash +#!/bin/bash +# Record workflow for documentation + +agent-browser record start ./docs/how-to-login.webm + +agent-browser open https://app.example.com/login +agent-browser wait 1000 # Pause for visibility + +agent-browser snapshot -i +agent-browser fill @e1 "demo@example.com" +agent-browser wait 500 + +agent-browser fill @e2 "password" +agent-browser wait 500 + +agent-browser click @e3 +agent-browser wait --load networkidle +agent-browser wait 1000 # Show result + +agent-browser record stop +``` + +### CI/CD Test Evidence + +```bash +#!/bin/bash +# Record E2E test runs for CI artifacts + +TEST_NAME="${1:-e2e-test}" +RECORDING_DIR="./test-recordings" +mkdir -p "$RECORDING_DIR" + +agent-browser record start "$RECORDING_DIR/$TEST_NAME-$(date +%s).webm" + +# Run test +if run_e2e_test; then + echo "Test passed" +else + echo "Test failed - recording saved" +fi + +agent-browser record stop +``` + +## Best Practices + +### 1. Add Pauses for Clarity + +```bash +# Slow down for human viewing +agent-browser click @e1 +agent-browser wait 500 # Let viewer see result +``` + +### 2. Use Descriptive Filenames + +```bash +# Include context in filename +agent-browser record start ./recordings/login-flow-2024-01-15.webm +agent-browser record start ./recordings/checkout-test-run-42.webm +``` + +### 3. Handle Recording in Error Cases + +```bash +#!/bin/bash +set -e + +cleanup() { + agent-browser record stop 2>/dev/null || true + agent-browser close 2>/dev/null || true +} +trap cleanup EXIT + +agent-browser record start ./automation.webm +# ... automation steps ... +``` + +### 4. Combine with Screenshots + +```bash +# Record video AND capture key frames +agent-browser record start ./flow.webm + +agent-browser open https://example.com +agent-browser screenshot ./screenshots/step1-homepage.png + +agent-browser click @e1 +agent-browser screenshot ./screenshots/step2-after-click.png + +agent-browser record stop +``` + +## Output Format + +- Default format: WebM (VP8/VP9 codec) +- Compatible with all modern browsers and video players +- Compressed but high quality + +## Limitations + +- Recording adds slight overhead to automation +- Large recordings can consume significant disk space +- Some headless environments may have codec limitations diff --git a/.agents/skills/agent-browser/templates/authenticated-session.sh b/.agents/skills/agent-browser/templates/authenticated-session.sh new file mode 100755 index 00000000..ebbfc1fa --- /dev/null +++ b/.agents/skills/agent-browser/templates/authenticated-session.sh @@ -0,0 +1,97 @@ +#!/bin/bash +# Template: Authenticated Session Workflow +# Purpose: Login once, save state, reuse for subsequent runs +# Usage: ./authenticated-session.sh [state-file] +# +# Environment variables: +# APP_USERNAME - Login username/email +# APP_PASSWORD - Login password +# +# Two modes: +# 1. Discovery mode (default): Shows form structure so you can identify refs +# 2. Login mode: Performs actual login after you update the refs +# +# Setup steps: +# 1. Run once to see form structure (discovery mode) +# 2. Update refs in LOGIN FLOW section below +# 3. Set APP_USERNAME and APP_PASSWORD +# 4. Delete the DISCOVERY section + +set -euo pipefail + +LOGIN_URL="${1:?Usage: $0 [state-file]}" +STATE_FILE="${2:-./auth-state.json}" + +echo "Authentication workflow: $LOGIN_URL" + +# ================================================================ +# SAVED STATE: Skip login if valid saved state exists +# ================================================================ +if [[ -f "$STATE_FILE" ]]; then + echo "Loading saved state from $STATE_FILE..." + agent-browser state load "$STATE_FILE" + agent-browser open "$LOGIN_URL" + agent-browser wait --load networkidle + + CURRENT_URL=$(agent-browser get url) + if [[ "$CURRENT_URL" != *"login"* ]] && [[ "$CURRENT_URL" != *"signin"* ]]; then + echo "Session restored successfully" + agent-browser snapshot -i + exit 0 + fi + echo "Session expired, performing fresh login..." + rm -f "$STATE_FILE" +fi + +# ================================================================ +# DISCOVERY MODE: Shows form structure (delete after setup) +# ================================================================ +echo "Opening login page..." +agent-browser open "$LOGIN_URL" +agent-browser wait --load networkidle + +echo "" +echo "Login form structure:" +echo "---" +agent-browser snapshot -i +echo "---" +echo "" +echo "Next steps:" +echo " 1. Note the refs: username=@e?, password=@e?, submit=@e?" +echo " 2. Update the LOGIN FLOW section below with your refs" +echo " 3. Set: export APP_USERNAME='...' APP_PASSWORD='...'" +echo " 4. Delete this DISCOVERY MODE section" +echo "" +agent-browser close +exit 0 + +# ================================================================ +# LOGIN FLOW: Uncomment and customize after discovery +# ================================================================ +# : "${APP_USERNAME:?Set APP_USERNAME environment variable}" +# : "${APP_PASSWORD:?Set APP_PASSWORD environment variable}" +# +# agent-browser open "$LOGIN_URL" +# agent-browser wait --load networkidle +# agent-browser snapshot -i +# +# # Fill credentials (update refs to match your form) +# agent-browser fill @e1 "$APP_USERNAME" +# agent-browser fill @e2 "$APP_PASSWORD" +# agent-browser click @e3 +# agent-browser wait --load networkidle +# +# # Verify login succeeded +# FINAL_URL=$(agent-browser get url) +# if [[ "$FINAL_URL" == *"login"* ]] || [[ "$FINAL_URL" == *"signin"* ]]; then +# echo "Login failed - still on login page" +# agent-browser screenshot /tmp/login-failed.png +# agent-browser close +# exit 1 +# fi +# +# # Save state for future runs +# echo "Saving state to $STATE_FILE" +# agent-browser state save "$STATE_FILE" +# echo "Login successful" +# agent-browser snapshot -i diff --git a/.agents/skills/agent-browser/templates/capture-workflow.sh b/.agents/skills/agent-browser/templates/capture-workflow.sh new file mode 100755 index 00000000..3bc93ad0 --- /dev/null +++ b/.agents/skills/agent-browser/templates/capture-workflow.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# Template: Content Capture Workflow +# Purpose: Extract content from web pages (text, screenshots, PDF) +# Usage: ./capture-workflow.sh [output-dir] +# +# Outputs: +# - page-full.png: Full page screenshot +# - page-structure.txt: Page element structure with refs +# - page-text.txt: All text content +# - page.pdf: PDF version +# +# Optional: Load auth state for protected pages + +set -euo pipefail + +TARGET_URL="${1:?Usage: $0 [output-dir]}" +OUTPUT_DIR="${2:-.}" + +echo "Capturing: $TARGET_URL" +mkdir -p "$OUTPUT_DIR" + +# Optional: Load authentication state +# if [[ -f "./auth-state.json" ]]; then +# echo "Loading authentication state..." +# agent-browser state load "./auth-state.json" +# fi + +# Navigate to target +agent-browser open "$TARGET_URL" +agent-browser wait --load networkidle + +# Get metadata +TITLE=$(agent-browser get title) +URL=$(agent-browser get url) +echo "Title: $TITLE" +echo "URL: $URL" + +# Capture full page screenshot +agent-browser screenshot --full "$OUTPUT_DIR/page-full.png" +echo "Saved: $OUTPUT_DIR/page-full.png" + +# Get page structure with refs +agent-browser snapshot -i > "$OUTPUT_DIR/page-structure.txt" +echo "Saved: $OUTPUT_DIR/page-structure.txt" + +# Extract all text content +agent-browser get text body > "$OUTPUT_DIR/page-text.txt" +echo "Saved: $OUTPUT_DIR/page-text.txt" + +# Save as PDF +agent-browser pdf "$OUTPUT_DIR/page.pdf" +echo "Saved: $OUTPUT_DIR/page.pdf" + +# Optional: Extract specific elements using refs from structure +# agent-browser get text @e5 > "$OUTPUT_DIR/main-content.txt" + +# Optional: Handle infinite scroll pages +# for i in {1..5}; do +# agent-browser scroll down 1000 +# agent-browser wait 1000 +# done +# agent-browser screenshot --full "$OUTPUT_DIR/page-scrolled.png" + +# Cleanup +agent-browser close + +echo "" +echo "Capture complete:" +ls -la "$OUTPUT_DIR" diff --git a/.agents/skills/agent-browser/templates/form-automation.sh b/.agents/skills/agent-browser/templates/form-automation.sh new file mode 100755 index 00000000..6784fcd3 --- /dev/null +++ b/.agents/skills/agent-browser/templates/form-automation.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# Template: Form Automation Workflow +# Purpose: Fill and submit web forms with validation +# Usage: ./form-automation.sh +# +# This template demonstrates the snapshot-interact-verify pattern: +# 1. Navigate to form +# 2. Snapshot to get element refs +# 3. Fill fields using refs +# 4. Submit and verify result +# +# Customize: Update the refs (@e1, @e2, etc.) based on your form's snapshot output + +set -euo pipefail + +FORM_URL="${1:?Usage: $0 }" + +echo "Form automation: $FORM_URL" + +# Step 1: Navigate to form +agent-browser open "$FORM_URL" +agent-browser wait --load networkidle + +# Step 2: Snapshot to discover form elements +echo "" +echo "Form structure:" +agent-browser snapshot -i + +# Step 3: Fill form fields (customize these refs based on snapshot output) +# +# Common field types: +# agent-browser fill @e1 "John Doe" # Text input +# agent-browser fill @e2 "user@example.com" # Email input +# agent-browser fill @e3 "SecureP@ss123" # Password input +# agent-browser select @e4 "Option Value" # Dropdown +# agent-browser check @e5 # Checkbox +# agent-browser click @e6 # Radio button +# agent-browser fill @e7 "Multi-line text" # Textarea +# agent-browser upload @e8 /path/to/file.pdf # File upload +# +# Uncomment and modify: +# agent-browser fill @e1 "Test User" +# agent-browser fill @e2 "test@example.com" +# agent-browser click @e3 # Submit button + +# Step 4: Wait for submission +# agent-browser wait --load networkidle +# agent-browser wait --url "**/success" # Or wait for redirect + +# Step 5: Verify result +echo "" +echo "Result:" +agent-browser get url +agent-browser snapshot -i + +# Optional: Capture evidence +agent-browser screenshot /tmp/form-result.png +echo "Screenshot saved: /tmp/form-result.png" + +# Cleanup +agent-browser close +echo "Done" diff --git a/.agents/skills/neon-postgres/SKILL.md b/.agents/skills/neon-postgres/SKILL.md new file mode 100644 index 00000000..7d5f93f6 --- /dev/null +++ b/.agents/skills/neon-postgres/SKILL.md @@ -0,0 +1,72 @@ +--- +name: neon-postgres +description: Guides and best practices for working with Neon Serverless Postgres. Covers getting started, local development with Neon, choosing a connection method, Neon features, authentication (@neondatabase/auth), PostgREST-style data API (@neondatabase/neon-js), Neon CLI, and Neon's Platform API/SDKs. Use for any Neon-related questions. +--- + +# Neon Serverless Postgres + +Neon is a serverless Postgres platform that separates compute and storage to offer autoscaling, branching, instant restore, and scale-to-zero. It's fully compatible with Postgres and works with any language, framework, or ORM that supports Postgres. + +## Neon Documentation + +Always reference the Neon documentation before making Neon-related claims. The documentation is the source of truth for all Neon-related information. + +Below you'll find a list of resources organized by area of concern. This is meant to support you find the right documentation pages to fetch and add a bit of additional context. + +You can use the `curl` commands to fetch the documentation page as markdown: + +**Documentation:** + +```bash +# Get list of all Neon docs +curl https://neon.com/llms.txt + +# Fetch any doc page as markdown +curl -H "Accept: text/markdown" https://neon.com/docs/ +``` + +Don't guess docs pages. Use the `llms.txt` index to find the relevant URL or follow the links in the resources below. + +## Overview of Resources + +Reference the appropriate resource file based on the user's needs: + +### Core Guides + +| Area | Resource | When to Use | +| ------------------ | ---------------------------------- | -------------------------------------------------------------- | +| What is Neon | `references/what-is-neon.md` | Understanding Neon concepts, architecture, core resources | +| Referencing Docs | `references/referencing-docs.md` | Looking up official documentation, verifying information | +| Features | `references/features.md` | Branching, autoscaling, scale-to-zero, instant restore | +| Getting Started | `references/getting-started.md` | Setting up a project, connection strings, dependencies, schema | +| Connection Methods | `references/connection-methods.md` | Choosing drivers based on platform and runtime | +| Developer Tools | `references/devtools.md` | VSCode extension, MCP server, Neon CLI (`neon init`) | + +### Database Drivers & ORMs + +HTTP/WebSocket queries for serverless/edge functions. + +| Area | Resource | When to Use | +| ----------------- | ------------------------------- | --------------------------------------------------- | +| Serverless Driver | `references/neon-serverless.md` | `@neondatabase/serverless` - HTTP/WebSocket queries | +| Drizzle ORM | `references/neon-drizzle.md` | Drizzle ORM integration with Neon | + +### Auth & Data API SDKs + +Authentication and PostgREST-style data API for Neon. + +| Area | Resource | When to Use | +| ----------- | ------------------------- | ------------------------------------------------------------------- | +| Neon Auth | `references/neon-auth.md` | `@neondatabase/auth` - Authentication only | +| Neon JS SDK | `references/neon-js.md` | `@neondatabase/neon-js` - Auth + Data API (PostgREST-style queries) | + +### Neon Platform API & CLI + +Managing Neon resources programmatically via REST API, SDKs, or CLI. + +| Area | Resource | When to Use | +| --------------------- | ----------------------------------- | -------------------------------------------- | +| Platform API Overview | `references/neon-platform-api.md` | Managing Neon resources via REST API | +| Neon CLI | `references/neon-cli.md` | Terminal workflows, scripts, CI/CD pipelines | +| TypeScript SDK | `references/neon-typescript-sdk.md` | `@neondatabase/api-client` | +| Python SDK | `references/neon-python-sdk.md` | `neon-api` package | diff --git a/.agents/skills/neon-postgres/references/connection-methods.md b/.agents/skills/neon-postgres/references/connection-methods.md new file mode 100644 index 00000000..604adc7c --- /dev/null +++ b/.agents/skills/neon-postgres/references/connection-methods.md @@ -0,0 +1,193 @@ +# Connection Methods + +Guide to selecting the optimal connection method for your Neon Postgres database based on deployment platform and runtime environment. + +For official documentation: + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/connect/choose-connection +``` + +## Decision Tree + +Follow this flow to determine the right connection approach: + +### 1. What Language Are You Using? + +**Not TypeScript/JavaScript** → Use **TCP with connection pooling** from a secure server. + +For non-TypeScript languages, connect from a secure backend server using your language's native Postgres driver with connection pooling enabled. + +| Language/Framework | Documentation | +| ------------------- | ------------------------------------------- | +| Django (Python) | https://neon.com/docs/guides/django | +| SQLAlchemy (Python) | https://neon.com/docs/guides/sqlalchemy | +| Elixir Ecto | https://neon.com/docs/guides/elixir-ecto | +| Laravel (PHP) | https://neon.com/docs/guides/laravel | +| Ruby on Rails | https://neon.com/docs/guides/ruby-on-rails | +| Go | https://neon.com/docs/guides/go | +| Rust | https://neon.com/docs/guides/rust | +| Java | https://neon.com/docs/guides/java | + +**TypeScript/JavaScript** → Continue to step 2. + +--- + +### 2. Client-Side App Without Backend? + +**Yes** → Use **Neon Data API** via `@neondatabase/neon-js` + +This is the only option for client-side apps since browsers cannot make direct TCP connections to Postgres. See `neon-js.md` for setup. + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/reference/javascript-sdk +``` + +**No** → Continue to step 3. + +--- + +### 3. Long-Running Server? (Railway, Render, traditional VPS) + +**Yes** → Use **TCP with connection pooling** via `node-postgres`, `postgres.js`, or `bun:pg` + +Long-running servers maintain persistent connections, so standard TCP drivers with pooling are optimal. + +**No** → Continue to step 4. + +--- + +### 4. Edge Environment Without TCP Support? + +Some edge runtimes don't support TCP connections. Rarely the case anymore. + +**Yes** → Continue to step 5 to check transaction requirements. + +**No** → Continue to step 6 to check pooling support. + +--- + +### 5. Does Your App Use SQL Transactions? + +**Yes** → Use **WebSocket transport** via `@neondatabase/serverless` with `Pool` + +WebSocket maintains connection state needed for transactions. See `neon-serverless.md` for setup. + +**No** → Use **HTTP transport** via `@neondatabase/serverless` + +HTTP is faster for single queries (~3 roundtrips vs ~8 for TCP). See `neon-serverless.md` for setup. + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/serverless/serverless-driver +``` + +--- + +### 6. Serverless Environment With Connection Pooling Support? + +**Vercel (Fluid Compute)** → Use **TCP with `@vercel/functions`** + +Vercel's Fluid compute supports connection pooling. Use `attachDatabasePool` for optimal connection management. + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/guides/vercel-connection-methods +``` + +**Cloudflare (with Hyperdrive)** → Use **TCP via Hyperdrive** + +Cloudflare Hyperdrive provides connection pooling for Workers. Use `node-postgres` or any native TCP driver. + +See https://neon.com/docs/guides/cloudflare-hyperdrive for more on connecting with Cloudflare Workers and Hyperdrive. + +**No pooling support (Netlify, Deno Deploy)** → Use `@neondatabase/serverless` + +Fall back to the decision in step 5 based on transaction requirements. + +--- + +## Quick Reference Table + +| Platform | TCP Support | Pooling | Recommended Driver | +| ----------------------- | ----------- | ------------------- | -------------------------- | +| Vercel (Fluid) | Yes | `@vercel/functions` | `pg` (node-postgres) | +| Cloudflare (Hyperdrive) | Yes | Hyperdrive | `pg` (node-postgres) | +| Cloudflare Workers | No | No | `@neondatabase/serverless` | +| Netlify Functions | No | No | `@neondatabase/serverless` | +| Deno Deploy | No | No | `@neondatabase/serverless` | +| Railway / Render | Yes | Built-in | `pg` (node-postgres) | +| Client-side (browser) | No | N/A | `@neondatabase/neon-js` | + +--- + +## ORM Support + +Popular TypeScript/JavaScript ORMs all work with Neon: + +| ORM | Drivers Supported | Documentation | +| ------- | ----------------------------------------------- | ------------------------------------- | +| Drizzle | `pg`, `postgres.js`, `@neondatabase/serverless` | https://neon.com/docs/guides/drizzle | +| Kysely | `pg`, `postgres.js`, `@neondatabase/serverless` | https://neon.com/docs/guides/kysely | +| Prisma | `pg`, `@neondatabase/serverless` | https://neon.com/docs/guides/prisma | +| TypeORM | `pg` | https://neon.com/docs/guides/typeorm | + +All ORMs support both TCP drivers and Neon's serverless driver depending on your platform. + +For Drizzle ORM integration with Neon, see `neon-drizzle.md`. + +--- + +## Vercel Fluid + Drizzle Example + +Complete database client setup for Vercel with Drizzle ORM and connection pooling. See `neon-drizzle.md` for more examples. + +```typescript +// src/lib/db/client.ts +import { attachDatabasePool } from "@vercel/functions"; +import { drizzle } from "drizzle-orm/node-postgres"; +import { Pool } from "pg"; + +import * as schema from "./schema"; + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, +}); +attachDatabasePool(pool); + +export const db = drizzle({ client: pool, schema }); +``` + +**Why `attachDatabasePool`?** + +- First request establishes the TCP connection (~8 roundtrips) +- Subsequent requests reuse the connection instantly +- Ensures idle connections close gracefully before function suspension +- Prevents connection leaks in serverless environments + +--- + +## Gathering Requirements + +When helping a user choose their connection method, gather this information: + +1. **Deployment platform**: Where will the app run? (Vercel, Cloudflare, Netlify, Railway, browser, etc.) +2. **Runtime type**: Serverless functions, edge functions, or long-running server? +3. **Transaction requirements**: Does the app need SQL transactions? +4. **ORM preference**: Using Drizzle, Kysely, Prisma, or raw SQL? + +Then provide: + +- The recommended driver/package +- A working code example for their setup +- The correct npm install command + +--- + +## Documentation Resources + +| Topic | URL | +| -------------------------- | ------------------------------------------------------- | +| Choosing Connection Method | https://neon.com/docs/connect/choose-connection | +| Serverless Driver | https://neon.com/docs/serverless/serverless-driver | +| JavaScript SDK | https://neon.com/docs/reference/javascript-sdk | +| Connection Pooling | https://neon.com/docs/connect/connection-pooling | +| Vercel Connection Methods | https://neon.com/docs/guides/vercel-connection-methods | diff --git a/.agents/skills/neon-postgres/references/devtools.md b/.agents/skills/neon-postgres/references/devtools.md new file mode 100644 index 00000000..3b1c88a7 --- /dev/null +++ b/.agents/skills/neon-postgres/references/devtools.md @@ -0,0 +1,121 @@ +# Neon Developer Tools + +Neon provides developer tools to enhance your local development workflow, including a VSCode extension and MCP server for AI-assisted development. + +## Quick Setup with neon init + +The fastest way to set up all Neon developer tools: + +```bash +npx neon init +``` + +This command: + +- Installs the Neon VSCode extension +- Configures the Neon MCP server for AI assistants +- Sets up your local environment for Neon development + +For full CLI reference: + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/reference/cli-init +``` + +## VSCode Extension + +The Neon VSCode extension provides: + +- **Database Explorer**: Browse projects, branches, tables, and data +- **SQL Editor**: Write and execute queries with IntelliSense +- **Branch Management**: Create, switch, and manage database branches +- **Connection String Access**: Quick copy of connection strings + +**Install from VSCode:** + +1. Open Extensions (Cmd/Ctrl+Shift+X) +2. Search "Neon" +3. Install "Neon" by Neon + +**Or via command line:** + +```bash +code --install-extension neon.neon-vscode +``` + +For detailed documentation: + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/local/vscode-extension +``` + +## Neon MCP Server + +The Neon MCP (Model Context Protocol) server enables AI assistants like Claude, Cursor, and GitHub Copilot to interact with your Neon databases directly. + +### Capabilities + +The MCP server provides AI assistants with: + +- **Project Management**: List, create, describe, and delete projects +- **Branch Operations**: Create branches, compare schemas, reset from parent +- **SQL Execution**: Run queries and transactions +- **Schema Operations**: Describe tables, get database structure +- **Migrations**: Prepare and complete database migrations with safety checks +- **Query Tuning**: Analyze and optimize slow queries +- **Neon Auth**: Provision authentication for your branches + +### Setup + +**Option 1: Via neon init (Recommended)** + +```bash +npx neon init +``` + +**Option 2: Manual Configuration** + +Add to your AI assistant's MCP configuration: + +```json +{ + "mcpServers": { + "neon": { + "command": "npx", + "args": ["-y", "@neondatabase/mcp-server-neon"], + "env": { + "NEON_API_KEY": "your-api-key" + } + } + } +} +``` + +Get your API key from: https://console.neon.tech/app/settings/api-keys + +### Common MCP Operations + +| Operation | What It Does | +| ---------------------------- | ----------------------------- | +| `list_projects` | Show all Neon projects | +| `create_project` | Create a new project | +| `run_sql` | Execute SQL queries | +| `get_connection_string` | Get database connection URL | +| `create_branch` | Create a database branch | +| `prepare_database_migration` | Safely prepare schema changes | +| `provision_neon_auth` | Set up Neon Auth | + +For full MCP server documentation: + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/ai/neon-mcp-server +``` + +## Documentation Resources + +| Topic | URL | +| ------------------ | --------------------------------------------- | +| CLI Init Command | https://neon.com/docs/reference/cli-init | +| VSCode Extension | https://neon.com/docs/local/vscode-extension | +| MCP Server | https://neon.com/docs/ai/neon-mcp-server | +| Neon CLI Reference | https://neon.com/docs/reference/neon-cli | diff --git a/.agents/skills/neon-postgres/references/features.md b/.agents/skills/neon-postgres/references/features.md new file mode 100644 index 00000000..7be8fede --- /dev/null +++ b/.agents/skills/neon-postgres/references/features.md @@ -0,0 +1,152 @@ +# Neon Features + +Overview of Neon's key platform features. For detailed information, fetch the official docs. + +## Branching + +Create instant, copy-on-write clones of your database at any point in time. Branches are isolated environments perfect for development, testing, and preview deployments. + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/introduction/branching +``` + +**Key Points:** + +- Branches are instant (no data copying) +- Copy-on-write means branches only store changes from parent +- Use for: dev environments, staging, testing, preview deployments +- Branches can have their own compute endpoint + +**Use Cases:** + +| Use Case | Description | +| ------------------- | ------------------------------------------- | +| Development | Each developer gets isolated branch | +| Preview Deployments | Branch per PR/preview URL | +| Testing | Reset test data by recreating branch | +| Schema Migrations | Test migrations on branch before production | + +If the Neon MCP server is available, you can use it to list and create branches. Otherwise, refer to the Neon CLI or Platform API. + +## Autoscaling + +Neon automatically scales compute resources based on workload demand. + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/introduction/autoscaling +``` + +**Key Points:** + +- Scales between min and max compute units (CUs) +- Responds to CPU and memory pressure +- No manual intervention required +- Configure limits per project or endpoint + +## Scale to Zero + +Databases automatically suspend after a period of inactivity, reducing costs to storage-only. + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/introduction/scale-to-zero +``` + +**Key Points:** + +- Default suspend after 5 minutes of inactivity (configurable) +- First query after suspend has ~500ms cold start +- Storage is always maintained +- Perfect for dev/staging environments with intermittent use + +## Instant Restore + +Restore your database to any point within your retention window without backups. + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/introduction/branch-restore +``` + +**Key Points:** + +- Point-in-time recovery without pre-configured backups +- Restore window depends on plan (7-30 days) +- Create branches from any point in history +- Time Travel queries to view historical data + +## Read Replicas + +Create read-only compute endpoints to scale read workloads. + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/introduction/read-replicas +``` + +**Key Points:** + +- Read replicas share storage with primary (no data duplication) +- Instant creation +- Independent scaling from primary +- Use for: analytics, reporting, read-heavy workloads + +## Connection Pooling + +Built-in connection pooling via PgBouncer for efficient connection management. + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/connect/connection-pooling +``` + +**Key Points:** + +- Enabled by adding `-pooler` to endpoint hostname +- Transaction mode by default +- Supports up to 10,000 concurrent connections +- Essential for serverless environments + +## IP Allow Lists + +Restrict database access to specific IP addresses or ranges. + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/introduction/ip-allow +``` + +## Logical Replication + +Replicate data to/from external Postgres databases. + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/guides/logical-replication-guide +``` + +## Neon Auth + +Managed authentication that branches with your database. + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/auth/overview +``` + +**Key Points:** + +- Sign-in/sign-up with email, social providers (Google, GitHub) +- Session management +- UI components included +- Branches with your database + +For setup, see `neon-auth.md`. For auth + data API, see `neon-js.md`. + +## Feature Documentation Reference + +| Feature | Documentation | Resource | +| ------------------- | ------------------------------------------------------- | -------------- | +| Branching | https://neon.com/docs/introduction/branching | - | +| Autoscaling | https://neon.com/docs/introduction/autoscaling | - | +| Scale to Zero | https://neon.com/docs/introduction/scale-to-zero | - | +| Instant Restore | https://neon.com/docs/introduction/branch-restore | - | +| Read Replicas | https://neon.com/docs/introduction/read-replicas | - | +| Connection Pooling | https://neon.com/docs/connect/connection-pooling | - | +| IP Allow | https://neon.com/docs/introduction/ip-allow | - | +| Logical Replication | https://neon.com/docs/guides/logical-replication-guide | - | +| Neon Auth | https://neon.com/docs/auth/overview | `neon-auth.md` | +| Data API | https://neon.com/docs/data-api/overview | `neon-js.md` | diff --git a/.agents/skills/neon-postgres/references/getting-started.md b/.agents/skills/neon-postgres/references/getting-started.md new file mode 100644 index 00000000..ad3a4eb9 --- /dev/null +++ b/.agents/skills/neon-postgres/references/getting-started.md @@ -0,0 +1,183 @@ +# Getting Started with Neon + +Interactive guide to help users get started with Neon in their project. Sets up their Neon project (with a connection string) and connects their database to their code. + +For the official getting started guide: + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/get-started/signing-up +``` + +## Interactive Setup Flow + +### Step 1: Check Organizations and Projects + +**First, check for organizations:** + +- If they have 1 organization: Default to that organization +- If they have multiple organizations: List all and ask which one to use + +**Then, check for projects within the selected organization:** + +- **No projects**: Ask if they want to create a new project +- **1 project**: Ask "Would you like to use '{project_name}' or create a new one?" +- **Multiple projects (<6)**: List all and let them choose +- **Many projects (6+)**: List recent projects, offer to create new or specify by name/ID + +### Step 2: Database Setup + +**Get the connection string:** + +- Use the MCP server to get the connection string for the selected project + +**Configure it for their environment:** + +- Most projects use a `.env` file with `DATABASE_URL` +- For other setups, check project structure and ask + +**Before modifying .env:** + +1. Try to read the .env file first +2. If readable: Use search_replace to update or append +3. If unreadable: Use append command or show the line to add manually: + +``` +DATABASE_URL=postgresql://user:password@host/database +``` + +### Step 3: Install Dependencies + +Recommend drivers based on deployment platform and runtime. For detailed guidance, see `connection-methods.md`. + +**Quick Recommendations:** + +| Environment | Driver | Install | +| ------------------------ | -------------------------- | -------------------------------------- | +| Vercel (Edge/Serverless) | `@neondatabase/serverless` | `npm install @neondatabase/serverless` | +| Cloudflare Workers | `@neondatabase/serverless` | `npm install @neondatabase/serverless` | +| AWS Lambda | `@neondatabase/serverless` | `npm install @neondatabase/serverless` | +| Traditional Node.js | `pg` | `npm install pg` | +| Long-running servers | `pg` with pooling | `npm install pg` | + +For detailed serverless driver usage, see `neon-serverless.md`. +For complex scenarios (multiple runtimes, hybrid architectures), reference `connection-methods.md`. + +### Step 4: Understand the Project + +**If it's an empty/new project:** +Ask briefly (1-2 questions): + +- What are they building? +- Any specific technologies? + +**If it's an established project:** +Skip questions - infer from codebase. Update relevant code to use the driver. + +### Step 5: Authentication (Optional) + +**Skip if project doesn't need auth** (CLI tools, scripts, static sites). + +**If project could benefit from auth:** +Ask: "Does your app need user authentication? Neon Auth can handle sign-in/sign-up, social login, and session management." + +**If they want auth:** + +- Use MCP server `provision_neon_auth` tool +- Guide through framework-specific setup +- Configure environment variables +- Set up basic auth code + +For detailed auth setup, see `neon-auth.md`. For auth + database queries, see `neon-js.md`. + +### Step 6: ORM Setup + +**Check for existing ORM** (Prisma, Drizzle, TypeORM). + +**If no ORM found:** +Ask: "Want to set up an ORM for type-safe database queries?" + +If yes, suggest based on project. If no, proceed with raw SQL. + +For Drizzle ORM integration, see `neon-drizzle.md`. + +### Step 7: Schema Setup + +**Check for existing schema:** + +- SQL migration files +- ORM schemas (Prisma, Drizzle) +- Database initialization scripts + +**If existing schema found:** +Ask: "Found existing schema definitions. Want to migrate these to your Neon database?" + +**If no schema:** +Ask if they want to: + +1. Create a simple example schema (users table) +2. Design a custom schema together +3. Skip schema setup for now + +**Example schema:** + +```sql +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + email VARCHAR(255) UNIQUE NOT NULL, + name VARCHAR(255), + created_at TIMESTAMP DEFAULT NOW() +); +``` + +### Step 8: What's Next + +"You're all set! Here are some things I can help with: + +- Neon-specific features (branching, autoscaling, scale-to-zero) +- Connection pooling for production +- Writing queries or building API endpoints +- Database migrations and schema changes +- Performance optimization" + +## Security Best Practices + +1. Never commit connection strings to version control +2. Use environment variables for all credentials +3. Prefer SSL connections (default in Neon) +4. Use least-privilege database roles +5. Rotate API keys and passwords regularly + +## Resume Support + +If user says "Continue with Neon setup", check what's already configured: + +- MCP server connection +- .env file with DATABASE_URL +- Dependencies installed +- Schema created + +Then resume from where they left off. + +## Developer Tools + +For the best development experience, set up Neon's developer tools: + +```bash +npx neon init +``` + +This installs the VSCode extension and configures the MCP server for AI-assisted development. + +For detailed setup instructions, see `devtools.md`. + +## Documentation Resources + +| Topic | URL | +| ------------------ | --------------------------------------------------- | +| Getting Started | https://neon.com/docs/get-started/signing-up | +| Connecting to Neon | https://neon.com/docs/connect/connect-intro | +| Connection String | https://neon.com/docs/connect/connect-from-any-app | +| Frameworks Guide | https://neon.com/docs/get-started/frameworks | +| ORMs Guide | https://neon.com/docs/get-started/orms | +| VSCode Extension | https://neon.com/docs/local/vscode-extension | +| MCP Server | https://neon.com/docs/ai/neon-mcp-server | diff --git a/.agents/skills/neon-postgres/references/neon-auth.md b/.agents/skills/neon-postgres/references/neon-auth.md new file mode 100644 index 00000000..bfa3e248 --- /dev/null +++ b/.agents/skills/neon-postgres/references/neon-auth.md @@ -0,0 +1,141 @@ +# Neon Auth + +Neon Auth provides authentication for your application. It's available as: + +- `@neondatabase/auth` - Auth only (smaller bundle) +- `@neondatabase/neon-js` - Auth + Data API (full SDK, see `neon-js.md`) + +For official documentation: + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/auth/overview +``` + +## Package Selection + +| Need | Package | Bundle | +| ----------------------- | ----------------------- | ------- | +| Auth only | `@neondatabase/auth` | Smaller | +| Auth + Database queries | `@neondatabase/neon-js` | Full | + +## Installation + +```bash +# Auth only +npm install @neondatabase/auth + +# Auth + Data API +npm install @neondatabase/neon-js +``` + +## Quick Setup Patterns + +### Next.js App Router + +**1. API Route Handler:** + +```typescript +// app/api/auth/[...path]/route.ts +import { authApiHandler } from "@neondatabase/auth/next"; +export const { GET, POST } = authApiHandler(); +``` + +**2. Auth Client:** + +```typescript +// lib/auth/client.ts +import { createAuthClient } from "@neondatabase/auth/next"; +export const authClient = createAuthClient(); +``` + +**3. Use in Components:** + +```typescript +"use client"; +import { authClient } from "@/lib/auth/client"; + +function AuthStatus() { + const session = authClient.useSession(); + if (session.isPending) return

Loading...
; + if (!session.data) return ; + return
Hello, {session.data.user.name}
; +} +``` + +### React SPA + +```typescript +import { createAuthClient } from "@neondatabase/auth"; +import { BetterAuthReactAdapter } from "@neondatabase/auth/react/adapters"; + +const authClient = createAuthClient(import.meta.env.VITE_NEON_AUTH_URL, { + adapter: BetterAuthReactAdapter(), +}); +``` + +### Node.js Backend + +```typescript +import { createAuthClient } from "@neondatabase/auth"; + +const auth = createAuthClient(process.env.NEON_AUTH_URL!); +await auth.signIn.email({ email, password }); +const session = await auth.getSession(); +``` + +## Environment Variables + +```bash +# Next.js (.env.local) +NEON_AUTH_BASE_URL=https://ep-xxx.neonauth.c-2.us-east-2.aws.neon.build/dbname/auth +NEXT_PUBLIC_NEON_AUTH_URL=https://ep-xxx.neonauth.c-2.us-east-2.aws.neon.build/dbname/auth + +# Vite/React (.env) +VITE_NEON_AUTH_URL=https://ep-xxx.neonauth.c-2.us-east-2.aws.neon.build/dbname/auth +``` + +## Sub-Resources + +For detailed documentation: + +| Topic | Resource | +| ------------------------ | ------------------------------ | +| Next.js App Router setup | `neon-auth/setup-nextjs.md` | +| React SPA setup | `neon-auth/setup-react-spa.md` | +| Auth methods reference | `neon-auth/auth-methods.md` | +| UI components | `neon-auth/ui-components.md` | +| Common mistakes | `neon-auth/common-mistakes.md` | + +## Key Imports + +```typescript +// Auth client (Next.js) +import { authApiHandler, createAuthClient } from "@neondatabase/auth/next"; + +// Auth client (vanilla) +import { createAuthClient } from "@neondatabase/auth"; + +// React adapter (NOT from main entry) +import { BetterAuthReactAdapter } from "@neondatabase/auth/react/adapters"; + +// UI components +import { + NeonAuthUIProvider, + AuthView, + SignInForm, +} from "@neondatabase/auth/react/ui"; +import { authViewPaths } from "@neondatabase/auth/react/ui/server"; + +// CSS +import "@neondatabase/auth/ui/css"; +``` + +## Common Mistakes + +1. **Wrong adapter import**: Import `BetterAuthReactAdapter` from `auth/react/adapters` subpath +2. **Forgetting to call adapter**: Use `BetterAuthReactAdapter()` with parentheses +3. **Missing CSS**: Import from `ui/css` or `ui/tailwind` (not both) +4. **Missing "use client"**: Required for components using `useSession()` +5. **Wrong createAuthClient signature**: First arg is URL: `createAuthClient(url, { adapter })` + +See `neon-auth/common-mistakes.md` for detailed examples. diff --git a/.agents/skills/neon-postgres/references/neon-auth/auth-methods.md b/.agents/skills/neon-postgres/references/neon-auth/auth-methods.md new file mode 100644 index 00000000..0a8ada26 --- /dev/null +++ b/.agents/skills/neon-postgres/references/neon-auth/auth-methods.md @@ -0,0 +1,241 @@ +# Neon Auth - Auth Methods Reference + +Complete reference for authentication methods, session management, and error handling. + +## Auth Methods + +### Sign Up + +```typescript +await auth.signUp.email({ + email: "user@example.com", + password: "securepassword", + name: "John Doe", // Optional +}); +``` + +### Sign In + +```typescript +// Email/password +await auth.signIn.email({ + email: "user@example.com", + password: "securepassword", +}); + +// Social (Google, GitHub) +await auth.signIn.social({ + provider: "google", // or "github" + callbackURL: "/dashboard", +}); +``` + +### Sign Out + +```typescript +await auth.signOut(); +``` + +### Get Session + +```typescript +// Async (Node.js, server components) +const session = await auth.getSession(); + +// React hook (client components) +const session = auth.useSession(); +// Returns: { data: Session | null, isPending: boolean } +``` + +## Session Data Structure + +```typescript +interface Session { + user: { + id: string; + name: string | null; + email: string; + image: string | null; + emailVerified: boolean; + createdAt: Date; + updatedAt: Date; + }; + session: { + id: string; + expiresAt: Date; + token: string; + createdAt: Date; + updatedAt: Date; + userId: string; + }; +} +``` + +## Error Handling + +```typescript +const { error } = await auth.signIn.email({ email, password }); + +if (error) { + switch (error.code) { + case "INVALID_EMAIL_OR_PASSWORD": + showError("Invalid email or password"); + break; + case "EMAIL_NOT_VERIFIED": + showError("Please verify your email"); + break; + case "USER_NOT_FOUND": + showError("User not found"); + break; + case "TOO_MANY_REQUESTS": + showError("Too many attempts. Please wait."); + break; + default: + showError("Authentication failed"); + } +} +``` + +## Building Auth Pages + +### Use AuthView (Recommended for React Apps) + +For authentication pages, use the pre-built `AuthView` component instead of building custom forms. + +**What AuthView provides:** + +- Sign-in, sign-up, password reset, magic link pages +- Social providers (Google, GitHub) - requires TWO configurations: enable in Neon Console AND add `social` prop to NeonAuthUIProvider +- Form validation, error handling, loading states +- Consistent styling via CSS variables + +**Setup (Next.js App Router):** + +1. **Import CSS** (in `app/layout.tsx` or `app/globals.css`): + +```tsx +import "@neondatabase/auth/ui/css"; +``` + +2. **Wrap app with provider** (create `app/auth-provider.tsx`): + +```tsx +"use client"; +import { NeonAuthUIProvider } from "@neondatabase/auth/react/ui"; +import { authClient } from "@/lib/auth/client"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const router = useRouter(); + return ( + router.refresh()} + Link={Link} + > + {children} + + ); +} +``` + +3. **Create auth page** (`app/auth/[path]/page.tsx`): + +```tsx +import { AuthView } from "@neondatabase/auth/react/ui"; +import { authViewPaths } from "@neondatabase/auth/react/ui/server"; + +export function generateStaticParams() { + return Object.values(authViewPaths).map((path) => ({ path })); +} + +export default async function AuthPage({ + params, +}: { + params: Promise<{ path: string }>; +}) { + const { path } = await params; + return ; +} +``` + +**Result:** You now have `/auth/sign-in`, `/auth/sign-up`, `/auth/forgot-password`, etc. + +**Available paths:** `"sign-in"`, `"sign-up"`, `"forgot-password"`, `"reset-password"`, `"magic-link"`, `"two-factor"`, `"callback"`, `"sign-out"` + +### When to Use Low-Level Methods Instead + +Use `authClient.signIn.email()`, `authClient.signUp.email()` directly if: + +- **Node.js backend** - No React, server-side auth only +- **Custom design system** - Your design team provides form components +- **Mobile/CLI apps** - Non-web frontends +- **Headless auth** - Testing or non-standard flows + +For standard React web apps, **use AuthView**. + +### Common Anti-Pattern + +```tsx +// ❌ Don't build custom forms unless you have specific requirements +function CustomSignInPage() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + const { error } = await authClient.signIn.email({ email, password }); + if (error) setError(error.message); + setLoading(false); + }; + + // ... 50+ more lines of form JSX, validation, error display +} + +// ✅ Use AuthView instead - one component handles everything +; +``` + +## Styling + +Neon Auth UI **automatically inherits your app's existing theme**. If you have CSS variables like `--primary`, `--background`, etc. defined (from Tailwind, shadcn/ui, or custom CSS), auth components use them with no configuration. + +**Key features:** + +- **Automatic inheritance**: Uses your existing `--primary`, `--background`, etc. +- **No conflicts**: Auth styles are in `@layer neon-auth`, so your styles always win +- **Import order doesn't matter**: CSS layers handle priority automatically + +### Integration with shadcn/ui + +If you use shadcn/ui or similar libraries that define `--primary`, `--background`, etc., Neon Auth will automatically inherit those colors. No additional configuration needed. + +### Use Existing CSS Variables + +When creating custom components, use CSS variables for consistency: + +| Variable | Purpose | +| ----------------------------------- | ----------------------- | +| `--background`, `--foreground` | Page background/text | +| `--card`, `--card-foreground` | Card surfaces | +| `--primary`, `--primary-foreground` | Primary buttons/actions | +| `--muted`, `--muted-foreground` | Muted/subtle elements | +| `--border`, `--ring` | Borders and focus rings | +| `--radius` | Border radius | + +### Auth-Specific Customization + +To customize auth components differently from your main app, use `--neon-*` prefix: + +```css +:root { + --primary: oklch(0.55 0.25 250); /* Your app's blue */ + --neon-primary: oklch(0.55 0.18 145); /* Auth uses green */ +} +``` diff --git a/.agents/skills/neon-postgres/references/neon-auth/common-mistakes.md b/.agents/skills/neon-postgres/references/neon-auth/common-mistakes.md new file mode 100644 index 00000000..27e4a0f3 --- /dev/null +++ b/.agents/skills/neon-postgres/references/neon-auth/common-mistakes.md @@ -0,0 +1,225 @@ +# Neon Auth - Common Mistakes + +Reference guide for common mistakes when using `@neondatabase/auth` or `@neondatabase/neon-js`. + +## Import Mistakes + +### BetterAuthReactAdapter Subpath Requirement + +`BetterAuthReactAdapter` is **NOT** exported from the main package entry. You must import it from the subpath. + +**Wrong:** + +```typescript +// These will NOT work +import { BetterAuthReactAdapter } from "@neondatabase/neon-js"; +import { BetterAuthReactAdapter } from "@neondatabase/auth"; +``` + +**Correct:** + +```typescript +// For @neondatabase/neon-js +import { BetterAuthReactAdapter } from "@neondatabase/neon-js/auth/react/adapters"; + +// For @neondatabase/auth +import { BetterAuthReactAdapter } from "@neondatabase/auth/react/adapters"; +``` + +**Why:** The React adapter has React-specific dependencies and is tree-shaken out of the main bundle. Using subpath exports keeps the main bundle smaller for non-React environments. + +### Adapter Factory Functions + +All adapters are **factory functions** that must be called with `()`. + +**Wrong:** + +```typescript +const client = createClient({ + auth: { + adapter: BetterAuthReactAdapter, // Missing () + url: process.env.NEON_AUTH_URL!, + }, + dataApi: { url: process.env.NEON_DATA_API_URL! }, +}); +``` + +**Correct:** + +```typescript +const client = createClient({ + auth: { + adapter: BetterAuthReactAdapter(), // Called as function + url: process.env.NEON_AUTH_URL!, + }, + dataApi: { url: process.env.NEON_DATA_API_URL! }, +}); +``` + +This applies to all adapters: + +- `BetterAuthReactAdapter()` +- `BetterAuthVanillaAdapter()` +- `SupabaseAuthAdapter()` + +--- + +## CSS Import Mistakes + +Auth UI components require CSS. Choose **ONE** method based on your project. + +### With Tailwind v4 + +```css +/* In app/globals.css */ +@import "tailwindcss"; +@import "@neondatabase/neon-js/ui/tailwind"; +/* Or: @import '@neondatabase/auth/ui/tailwind'; */ +``` + +### Without Tailwind + +```typescript +// In app/layout.tsx +import "@neondatabase/neon-js/ui/css"; +// Or: import "@neondatabase/auth/ui/css"; +``` + +### Never Import Both + +**Wrong:** + +```css +/* Causes ~94KB of duplicate styles */ +@import "@neondatabase/neon-js/ui/css"; +@import "@neondatabase/neon-js/ui/tailwind"; +``` + +**Why:** The `ui/css` import includes pre-built CSS (~47KB). The `ui/tailwind` import provides Tailwind tokens (~2KB) that generate similar styles. Using both doubles your CSS bundle. + +--- + +## Configuration Mistakes + +### Wrong createAuthClient Signature + +The `createAuthClient` function takes the URL as the first argument, not as a property in an options object. + +**Wrong:** + +```typescript +// This will NOT work +createAuthClient({ baseURL: url }); +createAuthClient({ url: myUrl }); +``` + +**Correct:** + +```typescript +// Vanilla client - URL as first arg +createAuthClient(url); + +// With adapter - URL as first arg, options as second +createAuthClient(url, { adapter: BetterAuthReactAdapter() }); + +// Next.js client - no arguments (uses env vars automatically) +import { createAuthClient } from "@neondatabase/auth/next"; +const authClient = createAuthClient(); +``` + +### Missing Environment Variables + +**Required for Next.js:** + +```bash +# .env.local +NEON_AUTH_BASE_URL=https://ep-xxx.neonauth.c-2.us-east-2.aws.neon.build/dbname/auth +NEXT_PUBLIC_NEON_AUTH_URL=https://ep-xxx.neonauth.c-2.us-east-2.aws.neon.build/dbname/auth + +# For neon-js (auth + data) +NEON_DATA_API_URL=https://ep-xxx.apirest.c-2.us-east-2.aws.neon.build/dbname/rest/v1 +``` + +**Required for Vite/React SPA:** + +```bash +# .env +VITE_NEON_AUTH_URL=https://ep-xxx.neonauth.c-2.us-east-2.aws.neon.build/dbname/auth +VITE_NEON_DATA_API_URL=https://ep-xxx.apirest.c-2.us-east-2.aws.neon.build/dbname/rest/v1 +``` + +**Important:** + +- `NEON_AUTH_BASE_URL` - Server-side auth +- `NEXT_PUBLIC_*` prefix - Required for client-side access in Next.js +- `VITE_*` prefix - Required for client-side access in Vite +- Restart dev server after adding env vars + +--- + +## Usage Mistakes + +### Missing "use client" Directive + +Client components using `useSession()` need the `"use client"` directive. + +**Wrong:** + +```typescript +// Missing directive - will cause hydration errors +import { authClient } from "@/lib/auth/client"; + +function AuthStatus() { + const session = authClient.useSession(); + // ... +} +``` + +**Correct:** + +```typescript +"use client"; + +import { authClient } from "@/lib/auth/client"; + +function AuthStatus() { + const session = authClient.useSession(); + // ... +} +``` + +### Wrong API for Adapter + +Each adapter has its own API style. Don't mix them. + +**Wrong - BetterAuth API with SupabaseAuthAdapter:** + +```typescript +const client = createClient({ + auth: { adapter: SupabaseAuthAdapter(), url }, + dataApi: { url }, +}); + +// This won't work with SupabaseAuthAdapter +await client.auth.signIn.email({ email, password }); +``` + +**Correct - Supabase API with SupabaseAuthAdapter:** + +```typescript +const client = createClient({ + auth: { adapter: SupabaseAuthAdapter(), url }, + dataApi: { url }, +}); + +// Use Supabase-style methods +await client.auth.signInWithPassword({ email, password }); +``` + +**API Reference by Adapter:** + +| Adapter | Sign In | Sign Up | Get Session | +| ------------------------ | ----------------------------------------- | ----------------------------------- | ------------------------------- | +| BetterAuthVanillaAdapter | `signIn.email({ email, password })` | `signUp.email({ email, password })` | `getSession()` | +| BetterAuthReactAdapter | `signIn.email({ email, password })` | `signUp.email({ email, password })` | `useSession()` / `getSession()` | +| SupabaseAuthAdapter | `signInWithPassword({ email, password })` | `signUp({ email, password })` | `getSession()` | diff --git a/.agents/skills/neon-postgres/references/neon-auth/setup-nextjs.md b/.agents/skills/neon-postgres/references/neon-auth/setup-nextjs.md new file mode 100644 index 00000000..90f51ca1 --- /dev/null +++ b/.agents/skills/neon-postgres/references/neon-auth/setup-nextjs.md @@ -0,0 +1,110 @@ +# Neon Auth Setup - Next.js App Router + +Complete setup instructions for Neon Auth in Next.js App Router applications. + +--- + +## 1. Install Package + +```bash +npm install @neondatabase/auth +# Or: npm install @neondatabase/neon-js +``` + +## 2. Environment Variables + +Create or update `.env.local`: + +```bash +NEON_AUTH_BASE_URL=https://ep-xxx.neonauth.c-2.us-east-2.aws.neon.build/dbname/auth +NEXT_PUBLIC_NEON_AUTH_URL=https://ep-xxx.neonauth.c-2.us-east-2.aws.neon.build/dbname/auth +``` + +**Important:** Both variables are needed: + +- `NEON_AUTH_BASE_URL` - Used by server-side API routes +- `NEXT_PUBLIC_NEON_AUTH_URL` - Used by client-side components (prefixed with NEXT*PUBLIC*) + +**Where to find your Auth URL:** + +1. Go to your Neon project dashboard +2. Navigate to the "Auth" tab +3. Copy the Auth URL + +## 3. API Route Handler + +Create `app/api/auth/[...path]/route.ts`: + +```typescript +import { authApiHandler } from "@neondatabase/auth/next"; +// Or: import { authApiHandler } from "@neondatabase/neon-js/auth/next"; + +export const { GET, POST } = authApiHandler(); +``` + +This creates endpoints for: + +- `/api/auth/sign-in` - Sign in +- `/api/auth/sign-up` - Sign up +- `/api/auth/sign-out` - Sign out +- `/api/auth/session` - Get session +- And other auth-related endpoints + +## 4. Auth Client Configuration + +Create `lib/auth/client.ts`: + +```typescript +import { createAuthClient } from "@neondatabase/auth/next"; +// Or: import { createAuthClient } from "@neondatabase/neon-js/auth/next"; + +export const authClient = createAuthClient(); +``` + +## 5. Use in Components + +```typescript +"use client"; + +import { authClient } from "@/lib/auth/client"; + +function AuthStatus() { + const session = authClient.useSession(); + + if (session.isPending) return
Loading...
; + if (!session.data) return ; + + return ( +
+

Hello, {session.data.user.name}

+ +
+ ); +} + +function SignInButton() { + return ( + + ); +} +``` + +## 6. UI Provider Setup (Optional) + +For pre-built UI components (AuthView, UserButton, etc.), see `ui-components.md`. + +--- + +## Package Selection + +| Need | Package | Bundle Size | +| ----------------------- | ----------------------- | --------------- | +| Auth only | `@neondatabase/auth` | Smaller (~50KB) | +| Auth + Database queries | `@neondatabase/neon-js` | Full (~150KB) | + +**Recommendation:** Use `@neondatabase/auth` if you only need authentication. Use `@neondatabase/neon-js` if you also need PostgREST-style database queries. diff --git a/.agents/skills/neon-postgres/references/neon-auth/setup-react-spa.md b/.agents/skills/neon-postgres/references/neon-auth/setup-react-spa.md new file mode 100644 index 00000000..13a9e51d --- /dev/null +++ b/.agents/skills/neon-postgres/references/neon-auth/setup-react-spa.md @@ -0,0 +1,248 @@ +# Neon Auth Setup - React SPA (Vite) + +Complete setup instructions for Neon Auth in React Single Page Applications (Vite, Create React App, etc.). + +--- + +## 1. Install Package + +```bash +npm install @neondatabase/auth +# Or: npm install @neondatabase/neon-js +npm install react-router-dom # Required for UI components +``` + +## 2. Environment Variables + +Create or update `.env`: + +**For Vite:** + +```bash +VITE_NEON_AUTH_URL=https://ep-xxx.neonauth.c-2.us-east-2.aws.neon.build/dbname/auth +``` + +**For Create React App:** + +```bash +REACT_APP_NEON_AUTH_URL=https://ep-xxx.neonauth.c-2.us-east-2.aws.neon.build/dbname/auth +``` + +**Where to find your Auth URL:** + +1. Go to your Neon project dashboard +2. Navigate to the "Auth" tab +3. Copy the Auth URL + +## 3. Auth Client Configuration + +Create `src/lib/auth-client.ts`: + +**For `@neondatabase/auth`:** + +```typescript +import { createAuthClient } from "@neondatabase/auth"; +import { BetterAuthReactAdapter } from "@neondatabase/auth/react/adapters"; + +export const authClient = createAuthClient(import.meta.env.VITE_NEON_AUTH_URL, { + adapter: BetterAuthReactAdapter(), +}); +``` + +**For `@neondatabase/neon-js`:** + +```typescript +import { createClient } from "@neondatabase/neon-js"; +import { BetterAuthReactAdapter } from "@neondatabase/neon-js/auth/react/adapters"; + +export const client = createClient({ + auth: { + adapter: BetterAuthReactAdapter(), + url: import.meta.env.VITE_NEON_AUTH_URL, + }, + dataApi: { + url: import.meta.env.VITE_NEON_DATA_API_URL, + }, +}); + +export const authClient = client.auth; +``` + +**Critical:** + +- `BetterAuthReactAdapter` must be imported from the `/react/adapters` subpath +- The adapter must be called as a function: `BetterAuthReactAdapter()` + +## 4. Use in Components + +```typescript +import { authClient } from "./lib/auth-client"; + +function App() { + const session = authClient.useSession(); + + if (session.isPending) return
Loading...
; + if (!session.data) return ; + + return ; +} +``` + +--- + +## 5. UI Provider Setup (Optional) + +Skip this section if you're building custom auth forms. Use this if you want pre-built UI components. + +### 5a. Import CSS + +**CRITICAL:** Choose ONE import method. Never import both - it causes duplicate styles. + +**Check if the project uses Tailwind CSS** by looking for: + +- `tailwind.config.js` or `tailwind.config.ts` in the project root +- `@import 'tailwindcss'` or `@tailwind` directives in CSS files +- `tailwindcss` in package.json dependencies + +**If NOT using Tailwind** - Add to `src/main.tsx` or entry point: + +```typescript +import "@neondatabase/auth/ui/css"; +``` + +**If using Tailwind CSS v4** - Add to main CSS file (e.g., index.css): + +```css +@import "tailwindcss"; +@import "@neondatabase/auth/ui/tailwind"; +``` + +### 5b. Update main.tsx with BrowserRouter + +```tsx +import { createRoot } from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; +import "@neondatabase/auth/ui/css"; // if not using Tailwind +import App from "./App"; +import { Providers } from "./providers"; + +createRoot(document.getElementById("root")!).render( + + + + + , +); +``` + +### 5c. Create Auth Provider + +Create `src/providers.tsx`: + +```tsx +import { NeonAuthUIProvider } from "@neondatabase/auth/react/ui"; +import { useNavigate, Link as RouterLink } from "react-router-dom"; +import { authClient } from "./lib/auth-client"; +import type { ReactNode } from "react"; + +// Adapter for react-router-dom Link +function Link({ + href, + ...props +}: { href: string } & React.AnchorHTMLAttributes) { + return ; +} + +export function Providers({ children }: { children: ReactNode }) { + const navigate = useNavigate(); + + return ( + navigate(path)} + replace={(path) => navigate(path, { replace: true })} + onSessionChange={() => { + // Optional: refresh data or invalidate cache + }} + Link={Link} + social={{ + providers: ["google", "github"], + }} + > + {children} + + ); +} +``` + +**Provider props explained:** + +- `navigate`: Function to navigate to a new route +- `replace`: Function to replace current route (for redirects) +- `onSessionChange`: Callback when auth state changes (useful for cache invalidation) +- `Link`: Adapter component for react-router-dom's Link +- `social`: Show Google and GitHub sign-in buttons (both enabled by default in Neon) + +### 5d. Add Routes to App.tsx + +```tsx +import { Routes, Route, useParams } from "react-router-dom"; +import { + AuthView, + UserButton, + SignedIn, + SignedOut, +} from "@neondatabase/auth/react/ui"; + +// Auth page - handles /auth/sign-in, /auth/sign-up, etc. +function AuthPage() { + const { pathname } = useParams(); + return ( +
+ +
+ ); +} + +// Simple navbar example +function Navbar() { + return ( + + ); +} + +function HomePage() { + return
Welcome to My App!
; +} + +export default function App() { + return ( + <> + + + } /> + } /> + + + ); +} +``` + +**Auth routes created:** + +- `/auth/sign-in` - Sign in page +- `/auth/sign-up` - Sign up page +- `/auth/forgot-password` - Password reset request +- `/auth/reset-password` - Set new password +- `/auth/sign-out` - Sign out +- `/auth/callback` - OAuth callback (internal) diff --git a/.agents/skills/neon-postgres/references/neon-auth/ui-components.md b/.agents/skills/neon-postgres/references/neon-auth/ui-components.md new file mode 100644 index 00000000..1f85c419 --- /dev/null +++ b/.agents/skills/neon-postgres/references/neon-auth/ui-components.md @@ -0,0 +1,215 @@ +# Neon Auth - UI Components Reference + +Pre-built UI components for authentication flows. + +## Available Components + +- `AuthView` - Complete auth pages (sign-in, sign-up, forgot-password, etc.) - **use this first** +- `SignedIn` / `SignedOut` - Conditional rendering based on auth state +- `UserButton` - User avatar with dropdown menu +- `NeonAuthUIProvider` - Required wrapper for UI components + +## CSS Import + +**CRITICAL:** Choose ONE import method. Never import both. + +**Without Tailwind:** + +```typescript +// In app/layout.tsx or entry point +import "@neondatabase/auth/ui/css"; +``` + +**With Tailwind v4:** + +```css +/* In app/globals.css */ +@import "tailwindcss"; +@import "@neondatabase/auth/ui/tailwind"; +``` + +## NeonAuthUIProvider Setup + +### Next.js App Router + +```tsx +"use client"; +import { NeonAuthUIProvider } from "@neondatabase/auth/react/ui"; +import { authClient } from "@/lib/auth/client"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const router = useRouter(); + return ( + router.refresh()} + Link={Link} + social={{ + providers: ["google", "github"], + }} + > + {children} + + ); +} +``` + +### React SPA with react-router-dom + +```tsx +import { NeonAuthUIProvider } from "@neondatabase/auth/react/ui"; +import { useNavigate, Link as RouterLink } from "react-router-dom"; +import { authClient } from "./lib/auth-client"; + +function Link({ + href, + ...props +}: { href: string } & React.AnchorHTMLAttributes) { + return ; +} + +export function Providers({ children }: { children: React.ReactNode }) { + const navigate = useNavigate(); + + return ( + navigate(path)} + replace={(path) => navigate(path, { replace: true })} + onSessionChange={() => {}} + Link={Link} + social={{ + providers: ["google", "github"], + }} + > + {children} + + ); +} +``` + +## AuthView Component + +Renders complete authentication pages. + +### Next.js App Router + +Create `app/auth/[path]/page.tsx`: + +```tsx +import { AuthView } from "@neondatabase/auth/react/ui"; +import { authViewPaths } from "@neondatabase/auth/react/ui/server"; + +export function generateStaticParams() { + return Object.values(authViewPaths).map((path) => ({ path })); +} + +export default async function AuthPage({ + params, +}: { + params: Promise<{ path: string }>; +}) { + const { path } = await params; + return ; +} +``` + +### React SPA + +```tsx +import { Routes, Route, useParams } from "react-router-dom"; +import { AuthView } from "@neondatabase/auth/react/ui"; + +function AuthPage() { + const { pathname } = useParams(); + return ( +
+ +
+ ); +} + +export default function App() { + return ( + + } /> + } /> + + ); +} +``` + +### Available Auth Paths + +| Path | Purpose | +| ----------------- | ------------------------- | +| `sign-in` | Sign in page | +| `sign-up` | Sign up page | +| `forgot-password` | Password reset request | +| `reset-password` | Set new password | +| `magic-link` | Magic link sign in | +| `two-factor` | Two-factor authentication | +| `callback` | OAuth callback (internal) | +| `sign-out` | Sign out | + +## SignedIn / SignedOut Components + +Conditional rendering based on authentication state. + +```tsx +import { SignedIn, SignedOut, UserButton } from "@neondatabase/auth/react/ui"; + +function Navbar() { + return ( + + ); +} +``` + +## UserButton Component + +Displays user avatar with dropdown menu for account management. + +```tsx +import { UserButton } from "@neondatabase/auth/react/ui"; + +function Header() { + return ( +
+

My App

+ +
+ ); +} +``` + +## Social Login Configuration + +**Important:** Social providers require TWO configurations: + +1. **Enable in Neon Console** - Go to your project's Auth settings +2. **Add to NeonAuthUIProvider** - Pass `social` prop + +```tsx + +``` + +Without both configurations, social login buttons won't appear. diff --git a/.agents/skills/neon-postgres/references/neon-cli.md b/.agents/skills/neon-postgres/references/neon-cli.md new file mode 100644 index 00000000..135bfa1d --- /dev/null +++ b/.agents/skills/neon-postgres/references/neon-cli.md @@ -0,0 +1,158 @@ +# Neon CLI + +The Neon CLI is a command-line interface for managing Neon Serverless Postgres directly from your terminal. It provides the same capabilities as the Neon Platform API and is ideal for scripting, CI/CD pipelines, and developers who prefer terminal workflows. + +## Installation + +**macOS (Homebrew):** + +```bash +brew install neonctl +``` + +**npm (cross-platform):** + +```bash +npm install -g neonctl +``` + +## Authentication + +Authenticate with your Neon account: + +```bash +neonctl auth +``` + +This opens a browser for OAuth authentication and stores credentials locally. + +For CI/CD or non-interactive environments, use an API key: + +```bash +export NEON_API_KEY=your-api-key +``` + +Get your API key from: https://console.neon.tech/app/settings/api-keys + +## Common Commands + +### Project Management + +```bash +# List all projects +neonctl projects list + +# Create a new project +neonctl projects create --name my-project + +# Get project details +neonctl projects get + +# Delete a project +neonctl projects delete +``` + +### Branch Operations + +```bash +# List branches +neonctl branches list --project-id + +# Create a branch +neonctl branches create --project-id --name dev + +# Delete a branch +neonctl branches delete --project-id +``` + +### Connection Strings + +```bash +# Get connection string +neonctl connection-string --project-id + +# Get connection string for specific branch +neonctl connection-string --project-id --branch-id + +# Get pooled connection string +neonctl connection-string --project-id --pooled +``` + +### SQL Execution + +```bash +# Run SQL query +neonctl sql "SELECT * FROM users LIMIT 10" --project-id + +# Run SQL from file +neonctl sql --file schema.sql --project-id +``` + +### Database Management + +```bash +# List databases +neonctl databases list --project-id --branch-id + +# Create database +neonctl databases create --project-id --name mydb + +# List roles +neonctl roles list --project-id --branch-id +``` + +## Output Formats + +The CLI supports multiple output formats: + +```bash +# JSON output (default for scripting) +neonctl projects list --output json + +# Table output (human-readable) +neonctl projects list --output table + +# YAML output +neonctl projects list --output yaml +``` + +## CI/CD Integration + +Example GitHub Actions workflow: + +```yaml +- name: Create preview branch + env: + NEON_API_KEY: ${{ secrets.NEON_API_KEY }} + run: | + neonctl branches create \ + --project-id ${{ vars.NEON_PROJECT_ID }} \ + --name preview-${{ github.event.pull_request.number }} +``` + +## CLI vs MCP Server vs SDKs + +| Tool | Best For | +| -------------- | ------------------------------------------------- | +| Neon CLI | Terminal workflows, scripts, CI/CD pipelines | +| MCP Server | AI-assisted development with Claude, Cursor, etc. | +| TypeScript SDK | Programmatic access in Node.js/TypeScript apps | +| Python SDK | Programmatic access in Python applications | +| REST API | Direct HTTP integration in any language | + +## Documentation Resources + +| Topic | URL | +| -------------- | ------------------------------------------------------ | +| CLI Reference | https://neon.com/docs/reference/neon-cli | +| CLI Install | https://neon.com/docs/reference/cli-install | +| CLI Auth | https://neon.com/docs/reference/cli-auth | +| CLI Projects | https://neon.com/docs/reference/cli-projects | +| CLI Branches | https://neon.com/docs/reference/cli-branches | +| CLI Connection | https://neon.com/docs/reference/cli-connection-string | + +Fetch CLI documentation: + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/reference/neon-cli +``` diff --git a/.agents/skills/neon-postgres/references/neon-drizzle.md b/.agents/skills/neon-postgres/references/neon-drizzle.md new file mode 100644 index 00000000..6dd1e6f3 --- /dev/null +++ b/.agents/skills/neon-postgres/references/neon-drizzle.md @@ -0,0 +1,245 @@ +# Neon and Drizzle Integration + +Integration patterns, configurations, and optimizations for using **Drizzle ORM** with **Neon** Postgres. + +For official documentation: + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/guides/drizzle +``` + +## Choosing the Right Driver + +Drizzle ORM works with multiple Postgres drivers. See `connection-methods.md` for the full decision tree. + +| Platform | TCP Support | Pooling | Recommended Driver | +| ----------------------- | ----------- | ------------------- | -------------------------- | +| Vercel (Fluid) | Yes | `@vercel/functions` | `pg` (node-postgres) | +| Cloudflare (Hyperdrive) | Yes | Hyperdrive | `pg` (node-postgres) | +| Cloudflare Workers | No | No | `@neondatabase/serverless` | +| Netlify Functions | No | No | `@neondatabase/serverless` | +| Deno Deploy | No | No | `@neondatabase/serverless` | +| Railway / Render | Yes | Built-in | `pg` (node-postgres) | + +## Connection Setup + +### 1. TCP with node-postgres (Long-Running Servers) + +Best for Railway, Render, traditional VPS. + +```bash +npm install drizzle-orm pg +npm install -D drizzle-kit @types/pg dotenv +``` + +```typescript +// src/db.ts +import { drizzle } from "drizzle-orm/node-postgres"; +import { Pool } from "pg"; + +const pool = new Pool({ connectionString: process.env.DATABASE_URL }); +export const db = drizzle({ client: pool }); +``` + +### 2. Vercel Fluid Compute with Connection Pooling + +```bash +npm install drizzle-orm pg @vercel/functions +npm install -D drizzle-kit @types/pg +``` + +```typescript +// src/db.ts +import { attachDatabasePool } from "@vercel/functions"; +import { drizzle } from "drizzle-orm/node-postgres"; +import { Pool } from "pg"; +import * as schema from "./schema"; + +const pool = new Pool({ connectionString: process.env.DATABASE_URL }); +attachDatabasePool(pool); + +export const db = drizzle({ client: pool, schema }); +``` + +### 3. HTTP Adapter (Edge Without TCP) + +For Cloudflare Workers, Netlify Edge, Deno Deploy. Does NOT support interactive transactions. + +```bash +npm install drizzle-orm @neondatabase/serverless +npm install -D drizzle-kit dotenv +``` + +```typescript +// src/db.ts +import { drizzle } from "drizzle-orm/neon-http"; +import { neon } from "@neondatabase/serverless"; + +const sql = neon(process.env.DATABASE_URL!); +export const db = drizzle(sql); +``` + +### 4. WebSocket Adapter (Edge with Transactions) + +```bash +npm install drizzle-orm @neondatabase/serverless ws +npm install -D drizzle-kit dotenv @types/ws +``` + +```typescript +// src/db.ts +import { drizzle } from "drizzle-orm/neon-serverless"; +import { Pool, neonConfig } from "@neondatabase/serverless"; +import ws from "ws"; + +neonConfig.webSocketConstructor = ws; // Required for Node.js < v22 + +const pool = new Pool({ connectionString: process.env.DATABASE_URL }); +export const db = drizzle(pool); +``` + +## Drizzle Config + +```typescript +// drizzle.config.ts +import { config } from "dotenv"; +import { defineConfig } from "drizzle-kit"; + +config({ path: ".env.local" }); + +export default defineConfig({ + schema: "./src/schema.ts", + out: "./drizzle", + dialect: "postgresql", + dbCredentials: { + url: process.env.DATABASE_URL!, + }, +}); +``` + +## Migrations + +```bash +# Generate migrations +npx drizzle-kit generate + +# Apply migrations +npx drizzle-kit migrate +``` + +## Schema Definition + +```typescript +// src/schema.ts +import { pgTable, serial, text, integer, timestamp } from "drizzle-orm/pg-core"; + +export const usersTable = pgTable("users", { + id: serial("id").primaryKey(), + name: text("name").notNull(), + email: text("email").notNull().unique(), + role: text("role").default("user").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), +}); + +export type User = typeof usersTable.$inferSelect; +export type NewUser = typeof usersTable.$inferInsert; + +export const postsTable = pgTable("posts", { + id: serial("id").primaryKey(), + title: text("title").notNull(), + content: text("content").notNull(), + userId: integer("user_id") + .notNull() + .references(() => usersTable.id, { onDelete: "cascade" }), + createdAt: timestamp("created_at").defaultNow().notNull(), +}); + +export type Post = typeof postsTable.$inferSelect; +export type NewPost = typeof postsTable.$inferInsert; +``` + +## Query Patterns + +### Batch Inserts + +```typescript +export async function batchInsertUsers(users: NewUser[]) { + return db.insert(usersTable).values(users).returning(); +} +``` + +### Prepared Statements + +```typescript +import { sql } from "drizzle-orm"; + +export const getUsersByRolePrepared = db + .select() + .from(usersTable) + .where(sql`${usersTable.role} = $1`) + .prepare("get_users_by_role"); + +// Usage: getUsersByRolePrepared.execute(['admin']) +``` + +### Transactions + +```typescript +export async function createUserWithPosts(user: NewUser, posts: NewPost[]) { + return await db.transaction(async (tx) => { + const [newUser] = await tx.insert(usersTable).values(user).returning(); + + if (posts.length > 0) { + await tx.insert(postsTable).values( + posts.map((post) => ({ + ...post, + userId: newUser.id, + })), + ); + } + + return newUser; + }); +} +``` + +## Working with Neon Branches + +```typescript +import { drizzle } from "drizzle-orm/neon-http"; +import { neon } from "@neondatabase/serverless"; + +const getBranchUrl = () => { + const env = process.env.NODE_ENV; + if (env === "development") return process.env.DEV_DATABASE_URL; + if (env === "test") return process.env.TEST_DATABASE_URL; + return process.env.DATABASE_URL; +}; + +const sql = neon(getBranchUrl()!); +export const db = drizzle({ client: sql }); +``` + +## Error Handling + +```typescript +export async function safeNeonOperation( + operation: () => Promise, +): Promise { + try { + return await operation(); + } catch (error: any) { + if (error.message?.includes("connection pool timeout")) { + console.error("Neon connection pool timeout"); + } + throw error; + } +} +``` + +## Best Practices + +1. **Connection Management** - See `connection-methods.md` for platform-specific guidance +2. **Neon Features** - Utilize branching for development/testing (see `features.md`) +3. **Query Optimization** - Batch operations, use prepared statements +4. **Schema Design** - Leverage Postgres-specific features, use appropriate indexes diff --git a/.agents/skills/neon-postgres/references/neon-js.md b/.agents/skills/neon-postgres/references/neon-js.md new file mode 100644 index 00000000..9e4cdb16 --- /dev/null +++ b/.agents/skills/neon-postgres/references/neon-js.md @@ -0,0 +1,218 @@ +# Neon JS SDK + +The `@neondatabase/neon-js` SDK provides a unified client for Neon Auth and Data API. It combines authentication handling with PostgREST-compatible database queries. + +**Auth only?** Use `neon-auth.md` instead for smaller bundle size. + +For official documentation: + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/reference/javascript-sdk +``` + +## Package Selection + +| Use Case | Package | Notes | +| --------------- | ---------------------------- | ------------------- | +| Auth + Data API | `@neondatabase/neon-js` | Full SDK | +| Auth only | `@neondatabase/auth` | Smaller bundle | +| Data API only | `@neondatabase/postgrest-js` | Bring your own auth | + +## Installation + +```bash +npm install @neondatabase/neon-js +``` + +## Quick Setup Patterns + +### Next.js (Most Common) + +**1. API Route Handler:** + +```typescript +// app/api/auth/[...path]/route.ts +import { authApiHandler } from "@neondatabase/neon-js/auth/next"; +export const { GET, POST } = authApiHandler(); +``` + +**2. Auth Client:** + +```typescript +// lib/auth/client.ts +import { createAuthClient } from "@neondatabase/neon-js/auth/next"; +export const authClient = createAuthClient(); +``` + +**3. Database Client:** + +```typescript +// lib/db/client.ts +import { createClient } from "@neondatabase/neon-js"; +import type { Database } from "./database.types"; + +export const dbClient = createClient({ + auth: { url: process.env.NEXT_PUBLIC_NEON_AUTH_URL! }, + dataApi: { url: process.env.NEON_DATA_API_URL! }, +}); +``` + +### React SPA + +```typescript +import { createClient } from "@neondatabase/neon-js"; +import { BetterAuthReactAdapter } from "@neondatabase/neon-js/auth/react/adapters"; + +const client = createClient({ + auth: { + adapter: BetterAuthReactAdapter(), + url: import.meta.env.VITE_NEON_AUTH_URL, + }, + dataApi: { url: import.meta.env.VITE_NEON_DATA_API_URL }, +}); +``` + +### Node.js Backend + +```typescript +import { createClient } from "@neondatabase/neon-js"; + +const client = createClient({ + auth: { url: process.env.NEON_AUTH_URL! }, + dataApi: { url: process.env.NEON_DATA_API_URL! }, +}); +``` + +## Environment Variables + +```bash +# Next.js (.env.local) +NEON_AUTH_BASE_URL=https://ep-xxx.neonauth.c-2.us-east-2.aws.neon.build/dbname/auth +NEXT_PUBLIC_NEON_AUTH_URL=https://ep-xxx.neonauth.c-2.us-east-2.aws.neon.build/dbname/auth +NEON_DATA_API_URL=https://ep-xxx.apirest.c-2.us-east-2.aws.neon.build/dbname/rest/v1 + +# Vite/React (.env) +VITE_NEON_AUTH_URL=https://ep-xxx.neonauth.c-2.us-east-2.aws.neon.build/dbname/auth +VITE_NEON_DATA_API_URL=https://ep-xxx.apirest.c-2.us-east-2.aws.neon.build/dbname/rest/v1 +``` + +## Database Queries + +All query methods follow PostgREST syntax (same as Supabase): + +```typescript +// Select with filters +const { data } = await client + .from("items") + .select("id, name, status") + .eq("status", "active") + .order("created_at", { ascending: false }) + .limit(10); + +// Insert +const { data, error } = await client + .from("items") + .insert({ name: "New Item", status: "pending" }) + .select() + .single(); + +// Update +await client.from("items").update({ status: "completed" }).eq("id", 1); + +// Delete +await client.from("items").delete().eq("id", 1); +``` + +For complete Data API query reference, see `neon-js/data-api.md`. + +## Auth Methods + +### BetterAuth API (Default) + +```typescript +// Sign in/up +await client.auth.signIn.email({ email, password }); +await client.auth.signUp.email({ email, password, name }); +await client.auth.signOut(); + +// Get session +const session = await client.auth.getSession(); + +// Social sign-in +await client.auth.signIn.social({ + provider: "google", + callbackURL: "/dashboard", +}); +``` + +### Supabase-Compatible API + +```typescript +import { createClient, SupabaseAuthAdapter } from "@neondatabase/neon-js"; + +const client = createClient({ + auth: { adapter: SupabaseAuthAdapter(), url }, + dataApi: { url }, +}); + +await client.auth.signInWithPassword({ email, password }); +await client.auth.signUp({ email, password }); +const { + data: { session }, +} = await client.auth.getSession(); +``` + +## Sub-Resources + +| Topic | Resource | +| ---------------- | ---------------------------- | +| Data API queries | `neon-js/data-api.md` | +| Common mistakes | `neon-js/common-mistakes.md` | + +## Key Imports + +```typescript +// Main client +import { + createClient, + SupabaseAuthAdapter, + BetterAuthVanillaAdapter, +} from "@neondatabase/neon-js"; + +// Next.js integration +import { + authApiHandler, + createAuthClient, +} from "@neondatabase/neon-js/auth/next"; + +// React adapter (NOT from main entry - must use subpath) +import { BetterAuthReactAdapter } from "@neondatabase/neon-js/auth/react/adapters"; + +// UI components +import { + NeonAuthUIProvider, + AuthView, + SignInForm, +} from "@neondatabase/neon-js/auth/react/ui"; +import { authViewPaths } from "@neondatabase/neon-js/auth/react/ui/server"; + +// CSS (choose one) +import "@neondatabase/neon-js/ui/css"; // Without Tailwind +// @import '@neondatabase/neon-js/ui/tailwind'; // With Tailwind v4 (in CSS file) +``` + +## Generate Types + +```bash +npx neon-js gen-types --db-url "postgresql://..." --output src/types/database.ts +``` + +## Common Mistakes + +1. **Wrong adapter import**: Import `BetterAuthReactAdapter` from `auth/react/adapters` subpath +2. **Forgetting to call adapter**: Use `SupabaseAuthAdapter()` with parentheses +3. **Missing CSS import**: Import from `ui/css` or `ui/tailwind` (not both) +4. **Wrong package for auth-only**: Use `@neondatabase/auth` for smaller bundle +5. **Missing "use client"**: Required for auth client components + +See `neon-js/common-mistakes.md` for detailed examples. diff --git a/.agents/skills/neon-postgres/references/neon-js/common-mistakes.md b/.agents/skills/neon-postgres/references/neon-js/common-mistakes.md new file mode 100644 index 00000000..dc7b4df8 --- /dev/null +++ b/.agents/skills/neon-postgres/references/neon-js/common-mistakes.md @@ -0,0 +1,127 @@ +# Neon JS - Common Mistakes + +Reference guide for common mistakes when using `@neondatabase/neon-js`. + +## Import Mistakes + +### BetterAuthReactAdapter Subpath Requirement + +`BetterAuthReactAdapter` is **NOT** exported from the main package entry. + +**Wrong:** + +```typescript +import { BetterAuthReactAdapter } from "@neondatabase/neon-js"; +``` + +**Correct:** + +```typescript +import { BetterAuthReactAdapter } from "@neondatabase/neon-js/auth/react/adapters"; +``` + +### Adapter Factory Functions + +All adapters must be called with `()`. + +**Wrong:** + +```typescript +const client = createClient({ + auth: { + adapter: BetterAuthReactAdapter, // Missing () + url: process.env.NEON_AUTH_URL!, + }, + dataApi: { url: process.env.NEON_DATA_API_URL! }, +}); +``` + +**Correct:** + +```typescript +const client = createClient({ + auth: { + adapter: BetterAuthReactAdapter(), // Called as function + url: process.env.NEON_AUTH_URL!, + }, + dataApi: { url: process.env.NEON_DATA_API_URL! }, +}); +``` + +--- + +## CSS Import Mistakes + +Choose **ONE** CSS import method: + +**With Tailwind v4:** + +```css +@import "tailwindcss"; +@import "@neondatabase/neon-js/ui/tailwind"; +``` + +**Without Tailwind:** + +```typescript +import "@neondatabase/neon-js/ui/css"; +``` + +**Never import both** - causes duplicate styles. + +--- + +## Environment Variables + +**Required for Next.js:** + +```bash +# .env.local +NEON_AUTH_BASE_URL=https://ep-xxx.neonauth.c-2.us-east-2.aws.neon.build/dbname/auth +NEXT_PUBLIC_NEON_AUTH_URL=https://ep-xxx.neonauth.c-2.us-east-2.aws.neon.build/dbname/auth +NEON_DATA_API_URL=https://ep-xxx.apirest.c-2.us-east-2.aws.neon.build/dbname/rest/v1 +``` + +**Required for Vite/React SPA:** + +```bash +# .env +VITE_NEON_AUTH_URL=https://ep-xxx.neonauth.c-2.us-east-2.aws.neon.build/dbname/auth +VITE_NEON_DATA_API_URL=https://ep-xxx.apirest.c-2.us-east-2.aws.neon.build/dbname/rest/v1 +``` + +--- + +## Usage Mistakes + +### Missing "use client" Directive + +```typescript +"use client"; // Required! + +import { authClient } from "@/lib/auth/client"; + +function AuthStatus() { + const session = authClient.useSession(); + // ... +} +``` + +### Wrong API for Adapter + +| Adapter | Sign In | Sign Up | +| ---------------------- | ----------------------------------------- | ----------------------------------- | +| BetterAuthReactAdapter | `signIn.email({ email, password })` | `signUp.email({ email, password })` | +| SupabaseAuthAdapter | `signInWithPassword({ email, password })` | `signUp({ email, password })` | + +### Using neon-js for Auth Only + +If you only need auth (no database queries), use `@neondatabase/auth` for smaller bundle size: + +```bash +# Auth only - smaller bundle +npm install @neondatabase/auth + +# Auth + Data API - full SDK +npm install @neondatabase/neon-js +``` diff --git a/.agents/skills/neon-postgres/references/neon-js/data-api.md b/.agents/skills/neon-postgres/references/neon-js/data-api.md new file mode 100644 index 00000000..931e21fd --- /dev/null +++ b/.agents/skills/neon-postgres/references/neon-js/data-api.md @@ -0,0 +1,481 @@ +# Neon JS Data API Reference + +Complete reference for PostgREST-style database queries using `@neondatabase/neon-js`. + +## Client Setup + +### Next.js + +```typescript +// lib/db/client.ts +import { createClient } from "@neondatabase/neon-js"; +import type { Database } from "./database.types"; + +export const dbClient = createClient({ + auth: { url: process.env.NEXT_PUBLIC_NEON_AUTH_URL! }, + dataApi: { url: process.env.NEON_DATA_API_URL! }, +}); +``` + +### React SPA + +```typescript +import { createClient } from "@neondatabase/neon-js"; +import { BetterAuthReactAdapter } from "@neondatabase/neon-js/auth/react/adapters"; + +const client = createClient({ + auth: { + adapter: BetterAuthReactAdapter(), + url: import.meta.env.VITE_NEON_AUTH_URL, + }, + dataApi: { url: import.meta.env.VITE_NEON_DATA_API_URL }, +}); +``` + +### Node.js Backend + +```typescript +import { createClient } from "@neondatabase/neon-js"; + +const client = createClient({ + auth: { url: process.env.NEON_AUTH_URL! }, + dataApi: { url: process.env.NEON_DATA_API_URL! }, +}); +``` + +--- + +## Query Patterns + +All query methods follow PostgREST syntax (same as Supabase). + +### Select Queries + +**Basic select:** + +```typescript +const { data, error } = await client.from("items").select(); +``` + +**Select specific columns:** + +```typescript +const { data } = await client.from("items").select("id, name, status"); +``` + +**Select with filters:** + +```typescript +const { data } = await client + .from("items") + .select("id, name, status") + .eq("status", "active") + .order("created_at", { ascending: false }) + .limit(10); +``` + +**Select single row:** + +```typescript +const { data, error } = await client + .from("items") + .select("*") + .eq("id", 1) + .single(); +``` + +### Insert + +**Insert single row:** + +```typescript +const { data, error } = await client + .from("items") + .insert({ name: "New Item", status: "pending" }) + .select() + .single(); +``` + +**Insert multiple rows:** + +```typescript +const { data, error } = await client + .from("items") + .insert([ + { name: "Item 1", status: "pending" }, + { name: "Item 2", status: "pending" }, + ]) + .select(); +``` + +### Update + +**Update with filter:** + +```typescript +await client.from("items").update({ status: "completed" }).eq("id", 1); +``` + +**Update and return data:** + +```typescript +const { data, error } = await client + .from("items") + .update({ status: "completed" }) + .eq("id", 1) + .select() + .single(); +``` + +### Delete + +**Delete single row:** + +```typescript +await client.from("items").delete().eq("id", 1); +``` + +**Delete and return data:** + +```typescript +const { data, error } = await client + .from("items") + .delete() + .eq("id", 1) + .select() + .single(); +``` + +### Upsert + +```typescript +await client + .from("items") + .upsert({ id: 1, name: "Updated Item", status: "active" }); +``` + +--- + +## Filtering + +### Comparison Operators + +```typescript +// Equal +.eq("status", "active") + +// Not equal +.neq("status", "archived") + +// Greater than +.gt("price", 100) + +// Greater than or equal +.gte("price", 100) + +// Less than +.lt("price", 100) + +// Less than or equal +.lte("price", 100) + +// Like (pattern matching) +.like("name", "%item%") + +// ILike (case-insensitive) +.ilike("name", "%item%") + +// Is null +.is("deleted_at", null) + +// Is not null +.not("deleted_at", "is", null) + +// In array +.in("status", ["active", "pending"]) + +// Contains (for arrays/JSONB) +.contains("tags", ["important"]) +``` + +### Logical Operators + +```typescript +// AND (chained) +.eq("status", "active") +.gt("price", 100) + +// OR +.or("status.eq.active,price.gt.100") + +// NOT +.not("status", "eq", "archived") +``` + +### Ordering + +```typescript +// Ascending +.order("created_at", { ascending: true }) + +// Descending +.order("created_at", { ascending: false }) + +// Multiple columns +.order("status", { ascending: true }) +.order("created_at", { ascending: false }) +``` + +### Pagination + +```typescript +// Limit +.limit(10) + +// Range (offset + limit) +.range(0, 9) // First 10 items + +// Range for pagination +const page = 1; +const pageSize = 10; +.range((page - 1) * pageSize, page * pageSize - 1) +``` + +--- + +## Relationships + +### Select with Relationships + +**One-to-many:** + +```typescript +const { data } = await client + .from("posts") + .select("id, title, author:users(name, email)"); +``` + +**Many-to-many:** + +```typescript +const { data } = await client + .from("posts") + .select("id, title, tags:post_tags(tag:tags(name))"); +``` + +**Nested relationships:** + +```typescript +const { data } = await client.from("posts").select(` + id, + title, + author:users( + id, + name, + profile:profiles(bio, avatar) + ) + `); +``` + +--- + +## Type Generation + +Generate TypeScript types from your database schema: + +```bash +npx neon-js gen-types --db-url "postgresql://user:pass@host/db" --output src/types/database.ts +``` + +Or using environment variable: + +```bash +npx neon-js gen-types --db-url "$DATABASE_URL" --output lib/db/database.types.ts +``` + +**Use types in client:** + +```typescript +import { createClient } from "@neondatabase/neon-js"; +import type { Database } from "./database.types"; + +export const dbClient = createClient({ + auth: { url: process.env.NEXT_PUBLIC_NEON_AUTH_URL! }, + dataApi: { url: process.env.NEON_DATA_API_URL! }, +}); +``` + +**Benefits:** + +- Full TypeScript autocomplete for tables and columns +- Type-safe queries +- Compile-time error checking + +--- + +## Error Handling + +**Check for errors:** + +```typescript +const { data, error } = await client.from("items").select(); + +if (error) { + console.error("Database error:", error.message); + console.error("Error code:", error.code); + console.error("Error details:", error.details); + return; +} + +// Use data +console.log(data); +``` + +**Common error codes:** + +- `PGRST116` - No rows returned (when using `.single()`) +- `23505` - Unique violation +- `23503` - Foreign key violation +- `42P01` - Table does not exist + +--- + +## Usage Examples + +### Server Component (Next.js) + +```typescript +// app/posts/page.tsx +import { dbClient } from "@/lib/db/client"; + +export default async function PostsPage() { + const { data: posts, error } = await dbClient + .from("posts") + .select("id, title, created_at, author:users(name)") + .order("created_at", { ascending: false }) + .limit(10); + + if (error) return
Error loading posts
; + + return ( +
    + {posts?.map((post) => ( +
  • +

    {post.title}

    +

    By {post.author?.name}

    +
  • + ))} +
+ ); +} +``` + +### API Route (Next.js) + +```typescript +// app/api/posts/route.ts +import { dbClient } from "@/lib/db/client"; +import { NextResponse } from "next/server"; + +export async function GET() { + const { data, error } = await dbClient.from("posts").select(); + + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } + + return NextResponse.json(data); +} + +export async function POST(request: Request) { + const body = await request.json(); + + const { data, error } = await dbClient + .from("posts") + .insert(body) + .select() + .single(); + + if (error) { + return NextResponse.json({ error: error.message }, { status: 400 }); + } + + return NextResponse.json(data, { status: 201 }); +} +``` + +### Client Component (React) + +```typescript +"use client"; + +import { useEffect, useState } from "react"; +import { dbClient } from "@/lib/db/client"; + +export function ItemsList() { + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchItems() { + const { data, error } = await dbClient + .from("items") + .select("id, name, status") + .eq("status", "active"); + + if (error) { + console.error(error); + return; + } + + setItems(data || []); + setLoading(false); + } + + fetchItems(); + }, []); + + if (loading) return
Loading...
; + + return ( +
    + {items.map((item) => ( +
  • {item.name}
  • + ))} +
+ ); +} +``` + +--- + +## Supabase Migration + +The Neon JS SDK uses the same PostgREST API as Supabase, making migration straightforward: + +**Before (Supabase):** + +```typescript +import { createClient } from "@supabase/supabase-js"; + +const client = createClient(SUPABASE_URL, SUPABASE_KEY); +``` + +**After (Neon):** + +```typescript +import { createClient, SupabaseAuthAdapter } from "@neondatabase/neon-js"; + +const client = createClient({ + auth: { adapter: SupabaseAuthAdapter(), url: NEON_AUTH_URL }, + dataApi: { url: NEON_DATA_API_URL }, +}); +``` + +**Query syntax remains the same:** + +```typescript +// Works identically in both +await client.auth.signInWithPassword({ email, password }); +const { data } = await client.from("items").select(); +``` diff --git a/.agents/skills/neon-postgres/references/neon-platform-api.md b/.agents/skills/neon-postgres/references/neon-platform-api.md new file mode 100644 index 00000000..bce5e831 --- /dev/null +++ b/.agents/skills/neon-postgres/references/neon-platform-api.md @@ -0,0 +1,96 @@ +# Neon Platform API + +The Neon Platform API allows you to manage Neon projects, branches, databases, and resources programmatically. You can use the REST API directly or through official SDKs. + +## Options + +| Method | Package/URL | Best For | +| -------------- | ----------------------------------- | ------------------------------- | +| REST API | `https://console.neon.tech/api/v2/` | Any language, direct HTTP calls | +| TypeScript SDK | `@neondatabase/api-client` | Node.js, TypeScript projects | +| Python SDK | `neon-api` | Python scripts and applications | +| CLI | `neonctl` | Terminal-based management | + +## Documentation + +```bash +# REST API documentation +curl -H "Accept: text/markdown" https://neon.com/docs/reference/api-reference + +# TypeScript SDK +curl -H "Accept: text/markdown" https://neon.com/docs/reference/typescript-sdk + +# Python SDK +curl -H "Accept: text/markdown" https://neon.com/docs/reference/python-sdk + +# CLI +curl -H "Accept: text/markdown" https://neon.com/docs/reference/neon-cli +``` + +For the interactive API reference: https://api-docs.neon.tech/reference/getting-started-with-neon-api + +## Sub-Resources + +For detailed information, reference the appropriate sub-resource: + +### REST API Details + +| Topic | Resource | +| ----------------------------- | -------------------------------- | +| Guidelines, Auth, Rate Limits | `neon-rest-api/guidelines.md` | +| Projects | `neon-rest-api/projects.md` | +| Branches, Databases, Roles | `neon-rest-api/branches.md` | +| Compute Endpoints | `neon-rest-api/endpoints.md` | +| API Keys | `neon-rest-api/keys.md` | +| Operations | `neon-rest-api/operations.md` | +| Organizations | `neon-rest-api/organizations.md` | + +### SDKs + +| Language | Resource | +| ---------- | ------------------------ | +| TypeScript | `neon-typescript-sdk.md` | +| Python | `neon-python-sdk.md` | + +## Quick Start + +### Authentication + +All API requests require a Neon API key: + +```bash +Authorization: Bearer $NEON_API_KEY +``` + +### API Key Types + +| Type | Scope | Best For | +| -------------- | ------------------------------- | ----------------------------- | +| Personal | All projects user has access to | Individual use, scripting | +| Organization | Entire organization | CI/CD, org-wide automation | +| Project-scoped | Single project only | Project-specific integrations | + +### Rate Limits + +- 700 requests per minute (~11 per second) +- Bursts up to 40 requests per second per route +- Handle `429 Too Many Requests` with retry/backoff + +## Common Operations Quick Reference + +| Operation | REST API | TypeScript SDK | Python SDK | +| ------------------ | ------------------------------------------ | --------------------------- | --------------------- | +| List Projects | `GET /projects` | `listProjects({})` | `projects()` | +| Create Project | `POST /projects` | `createProject({...})` | `project_create(...)` | +| Get Connection URI | `GET /projects/{id}/connection_uri` | `getConnectionUri({...})` | `connection_uri(...)` | +| Create Branch | `POST /projects/{id}/branches` | `createProjectBranch(...)` | `branch_create(...)` | +| Start Endpoint | `POST /projects/{id}/endpoints/{id}/start` | `startProjectEndpoint(...)` | `endpoint_start(...)` | + +## Error Handling + +| Status | Meaning | Action | +| ------ | ------------ | ---------------------------- | +| 401 | Unauthorized | Check API key | +| 404 | Not Found | Verify resource ID | +| 429 | Rate Limited | Implement retry with backoff | +| 500 | Server Error | Retry or contact support | diff --git a/.agents/skills/neon-postgres/references/neon-python-sdk.md b/.agents/skills/neon-postgres/references/neon-python-sdk.md new file mode 100644 index 00000000..35ad38c6 --- /dev/null +++ b/.agents/skills/neon-postgres/references/neon-python-sdk.md @@ -0,0 +1,261 @@ +# Neon Python SDK + +The `neon-api` Python SDK is a Pythonic wrapper around the Neon REST API. It provides methods for managing all Neon resources, including projects, branches, endpoints, roles, and databases. + +For core concepts (Organization, Project, Branch, Endpoint, etc.), see `what-is-neon.md`. + +## Documentation + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/reference/python-sdk +``` + +## Installation + +```bash +pip install neon-api +``` + +## Authentication + +```python +import os +from neon_api import NeonAPI + +api_key = os.getenv("NEON_API_KEY") +if not api_key: + raise ValueError("NEON_API_KEY environment variable is not set.") + +neon = NeonAPI(api_key=api_key) +``` + +## Projects + +### List Projects + +```python +all_projects = neon.projects() +``` + +### Create Project + +```python +new_project = neon.project_create( + project={ + 'name': 'my-new-project', + 'pg_version': 17 + } +) +``` + +### Get Project Details + +```python +project = neon.project(project_id='your-project-id') +``` + +### Update Project + +```python +neon.project_update( + project_id='your-project-id', + project={ + 'name': 'renamed-project', + 'default_endpoint_settings': { + 'autoscaling_limit_min_cu': 1, + 'autoscaling_limit_max_cu': 2, + } + } +) +``` + +### Delete Project + +```python +neon.project_delete(project_id='project-to-delete') +``` + +### Get Connection URI + +```python +uri = neon.connection_uri( + project_id='your-project-id', + database_name='neondb', + role_name='neondb_owner' +) +print(f"Connection URI: {uri.uri}") +``` + +## Branches + +### Create Branch + +```python +new_branch = neon.branch_create( + project_id='your-project-id', + branch={'name': 'feature-branch'}, + endpoints=[ + {'type': 'read_write', 'autoscaling_limit_max_cu': 1} + ] +) +``` + +### List Branches + +```python +branches = neon.branches(project_id='your-project-id') +``` + +### Get Branch Details + +```python +branch = neon.branch(project_id='your-project-id', branch_id='br-xxx') +``` + +### Update Branch + +```python +neon.branch_update( + project_id='your-project-id', + branch_id='br-xxx', + branch={'name': 'updated-branch-name'} +) +``` + +### Delete Branch + +```python +neon.branch_delete(project_id='your-project-id', branch_id='br-xxx') +``` + +## Databases + +### Create Database + +```python +neon.database_create( + project_id='your-project-id', + branch_id='br-xxx', + database={'name': 'my-app-db', 'owner_name': 'neondb_owner'} +) +``` + +### List Databases + +```python +databases = neon.databases(project_id='your-project-id', branch_id='br-xxx') +``` + +### Delete Database + +```python +neon.database_delete( + project_id='your-project-id', + branch_id='br-xxx', + database_id='my-app-db' +) +``` + +## Roles + +### Create Role + +```python +new_role = neon.role_create( + project_id='your-project-id', + branch_id='br-xxx', + role_name='app_user' +) +print(f"Password: {new_role.role.password}") +``` + +### List Roles + +```python +roles = neon.roles(project_id='your-project-id', branch_id='br-xxx') +``` + +### Delete Role + +```python +neon.role_delete( + project_id='your-project-id', + branch_id='br-xxx', + role_name='app_user' +) +``` + +## Endpoints + +### Create Endpoint + +```python +neon.endpoint_create( + project_id='your-project-id', + endpoint={ + 'branch_id': 'br-xxx', + 'type': 'read_only' + } +) +``` + +### Start/Suspend Endpoint + +```python +# Start +neon.endpoint_start(project_id='your-project-id', endpoint_id='ep-xxx') + +# Suspend +neon.endpoint_suspend(project_id='your-project-id', endpoint_id='ep-xxx') +``` + +### Update Endpoint + +```python +neon.endpoint_update( + project_id='your-project-id', + endpoint_id='ep-xxx', + endpoint={'autoscaling_limit_max_cu': 2} +) +``` + +### Delete Endpoint + +```python +neon.endpoint_delete(project_id='your-project-id', endpoint_id='ep-xxx') +``` + +## API Keys + +### List API Keys + +```python +api_keys = neon.api_keys() +``` + +### Create API Key + +```python +new_key = neon.api_key_create(key_name='my-script-key') +print(f"Key (store securely!): {new_key.key}") +``` + +### Revoke API Key + +```python +neon.api_key_revoke(1234) # key ID +``` + +## Operations + +### List Operations + +```python +ops = neon.operations(project_id='your-project-id') +``` + +### Get Operation Details + +```python +op = neon.operation(project_id='your-project-id', operation_id='op-xxx') +``` diff --git a/.agents/skills/neon-postgres/references/neon-rest-api/branches.md b/.agents/skills/neon-postgres/references/neon-rest-api/branches.md new file mode 100644 index 00000000..9d24f028 --- /dev/null +++ b/.agents/skills/neon-postgres/references/neon-rest-api/branches.md @@ -0,0 +1,552 @@ +## Overview + +This document outlines the rules for managing branches in a Neon project using the Neon API. + +## Manage branches + +### Create branch + +1. Action: Creates a new branch within a specified project. By default, a branch is created from the project's default branch, but you can specify a parent branch, a point-in-time (LSN or timestamp), and attach compute endpoints. +2. Endpoint: `POST /projects/{project_id}/branches` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project where the branch will be created. +4. Body Parameters: The request body is optional. If provided, it can contain `endpoints` and/or `branch` objects. + + `endpoints` (array of objects, optional): A list of compute endpoints to create and attach to the new branch. + - `type` (string, required): The endpoint type. Allowed values: `read_write`, `read_only`. + - `autoscaling_limit_min_cu` (number, optional): The minimum number of Compute Units (CU). Minimum value is `0.25`. + - `autoscaling_limit_max_cu` (number, optional): The maximum number of Compute Units (CU). Minimum value is `0.25`. + - `provisioner` (string, optional): The compute provisioner. Specify `k8s-neonvm` to enable Autoscaling. Allowed values: `k8s-pod`, `k8s-neonvm`. + - `suspend_timeout_seconds` (integer, optional): Duration of inactivity in seconds before a compute is suspended. Ranges from -1 (never suspend) to 604800 (1 week). A value of `0` uses the default of 300 seconds (5 minutes). + + `branch` (object, optional): Specifies the properties of the new branch. + - `name` (string, optional): A name for the branch (max 256 characters). If omitted, a name is auto-generated. + - `parent_id` (string, optional): The ID of the parent branch. If omitted, the project's default branch is used as the parent. + - `parent_lsn` (string, optional): A Log Sequence Number (LSN) from the parent branch to create the new branch from a specific point-in-time. + - `parent_timestamp` (string, optional): An ISO 8601 timestamp (e.g., `2025-08-26T12:00:00Z`) to create the branch from a specific point-in-time. + - `protected` (boolean, optional): If `true`, the branch is created as a protected branch. + - `init_source` (string, optional): The source for branch initialization. `parent-data` (default) copies schema and data. `schema-only` creates a new root branch with only the schema from the specified parent. + - `expires_at` (string, optional): An RFC 3339 timestamp for when the branch should be automatically deleted (e.g., `2025-06-09T18:02:16Z`). + +Example: Create a branch from a specific parent with a read-write compute + +```bash +curl 'https://console.neon.tech/api/v2/projects/hidden-river-50598307/branches' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" \ + -H 'Content-Type: application/json' \ + -d '{ + "endpoints": [ + { + "type": "read_write" + } + ], + "branch": { + "parent_id": "br-super-wildflower-adniii9u", + "name": "my-new-feature-branch" + } +}' +``` + +Example response + +```json +{ + "branch": { + "id": "br-damp-glitter-adqd4hk5", + "project_id": "hidden-river-50598307", + "parent_id": "br-super-wildflower-adniii9u", + "parent_lsn": "0/1A7F730", + "name": "my-new-feature-branch", + "current_state": "init", + "pending_state": "ready", + "state_changed_at": "2025-09-10T16:45:52Z", + "creation_source": "console", + "primary": false, + "default": false, + "protected": false, + "cpu_used_sec": 0, + "compute_time_seconds": 0, + "active_time_seconds": 0, + "written_data_bytes": 0, + "data_transfer_bytes": 0, + "created_at": "2025-09-10T16:45:52Z", + "updated_at": "2025-09-10T16:45:52Z", + "created_by": { + "name": "", + "image": "" + }, + "init_source": "parent-data" + }, + "endpoints": [ + { + "host": "ep-raspy-glade-ad8e3gvy.c-2.us-east-1.aws.neon.tech", + "id": "ep-raspy-glade-ad8e3gvy", + "project_id": "hidden-river-50598307", + "branch_id": "br-damp-glitter-adqd4hk5", + "autoscaling_limit_min_cu": 0.25, + "autoscaling_limit_max_cu": 2, + "region_id": "aws-us-east-1", + "type": "read_write", + "current_state": "init", + "pending_state": "active", + "settings": {}, + "pooler_enabled": false, + "pooler_mode": "transaction", + "disabled": false, + "passwordless_access": true, + "creation_source": "console", + "created_at": "2025-09-10T16:45:52Z", + "updated_at": "2025-09-10T16:45:52Z", + "proxy_host": "c-2.us-east-1.aws.neon.tech", + "suspend_timeout_seconds": 0, + "provisioner": "k8s-neonvm" + } + ], + "operations": [ + { + "id": "cf5d0923-fc13-4125-83d5-8fc31c6b0214", + "project_id": "hidden-river-50598307", + "branch_id": "br-damp-glitter-adqd4hk5", + "action": "create_branch", + "status": "running", + "failures_count": 0, + "created_at": "2025-09-10T16:45:52Z", + "updated_at": "2025-09-10T16:45:52Z", + "total_duration_ms": 0 + }, + { + "id": "e3c60b62-00c8-4ad4-9cd1-cdc3e8fd8154", + "project_id": "hidden-river-50598307", + "branch_id": "br-damp-glitter-adqd4hk5", + "endpoint_id": "ep-raspy-glade-ad8e3gvy", + "action": "start_compute", + "status": "scheduling", + "failures_count": 0, + "created_at": "2025-09-10T16:45:52Z", + "updated_at": "2025-09-10T16:45:52Z", + "total_duration_ms": 0 + } + ], + "roles": [ + { + "branch_id": "br-damp-glitter-adqd4hk5", + "name": "neondb_owner", + "protected": false, + "created_at": "2025-09-10T12:14:58Z", + "updated_at": "2025-09-10T12:14:58Z" + } + ], + "databases": [ + { + "id": 9554148, + "branch_id": "br-damp-glitter-adqd4hk5", + "name": "neondb", + "owner_name": "neondb_owner", + "created_at": "2025-09-10T12:14:58Z", + "updated_at": "2025-09-10T12:14:58Z" + } + ], + "connection_uris": [ + { + "connection_uri": "postgresql://neondb_owner:npg_EwcS9IOgFfb7@ep-raspy-glade-ad8e3gvy.c-2.us-east-1.aws.neon.tech/neondb?sslmode=require", + "connection_parameters": { + "database": "neondb", + "password": "npg_EwcS9IOgFfb7", + "role": "neondb_owner", + "host": "ep-raspy-glade-ad8e3gvy.c-2.us-east-1.aws.neon.tech", + "pooler_host": "ep-raspy-glade-ad8e3gvy-pooler.c-2.us-east-1.aws.neon.tech" + } + } + ] +} +``` + +### List branches + +1. Action: Retrieves a list of branches for the specified project. Supports filtering, sorting, and pagination. +2. Endpoint: `GET /projects/{project_id}/branches` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project. +4. Query Parameters: + - `search` (string, optional): Filters branches by a partial match on name or ID. + - `sort_by` (string, optional): The field to sort by. Allowed values: `name`, `created_at`, `updated_at`. Defaults to `updated_at`. + - `sort_order` (string, optional): The sort order. Allowed values: `asc`, `desc`. Defaults to `desc`. + - `limit` (integer, optional): The number of branches to return (1 to 10000). + - `cursor` (string, optional): The cursor from a previous response for pagination. + +Example: List all branches sorted by creation date + +```bash +curl 'https://console.neon.tech/api/v2/projects/hidden-river-50598307/branches?sort_by=created_at&sort_order=asc' \ + -H 'accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" +``` + +Example response + +```json +{ + "branches": [ + { + "id": "br-long-feather-adpbgzlx", + "project_id": "hidden-river-50598307", + "name": "production", + "current_state": "ready", + "state_changed_at": "2025-09-10T12:15:01Z", + "logical_size": 30785536, + "creation_source": "console", + "primary": true, + "default": true, + "protected": false, + "cpu_used_sec": 82, + "compute_time_seconds": 82, + "active_time_seconds": 316, + "written_data_bytes": 29060360, + "data_transfer_bytes": 0, + "created_at": "2025-09-10T12:14:58Z", + "updated_at": "2025-09-10T12:35:33Z", + "created_by": { + "name": "", + "image": "" + }, + "init_source": "parent-data" + }, + { + "id": "br-super-wildflower-adniii9u", + "project_id": "hidden-river-50598307", + "parent_id": "br-long-feather-adpbgzlx", + "parent_lsn": "0/1A33BC8", + "parent_timestamp": "2025-09-10T12:15:03Z", + "name": "development", + "current_state": "ready", + "state_changed_at": "2025-09-10T12:15:04Z", + "logical_size": 30842880, + "creation_source": "console", + "primary": false, + "default": false, + "protected": false, + "cpu_used_sec": 78, + "compute_time_seconds": 78, + "active_time_seconds": 312, + "written_data_bytes": 310120, + "data_transfer_bytes": 0, + "created_at": "2025-09-10T12:15:04Z", + "updated_at": "2025-09-10T12:35:33Z", + "created_by": { + "name": "", + "image": "" + }, + "init_source": "parent-data" + } + ], + "annotations": { + "br-long-feather-adpbgzlx": { + "object": { + "type": "console/branch", + "id": "br-long-feather-adpbgzlx" + }, + "value": { + "environment": "production" + }, + "created_at": "2025-09-10T12:14:58Z", + "updated_at": "2025-09-10T12:14:58Z" + } + }, + "pagination": { + "sort_by": "created_at", + "sort_order": "ASC" + } +} +``` + +### Retrieve branch details + +1. Action: Retrieves detailed information about a specific branch, including its parent, creation timestamp, and state. +2. Endpoint: `GET /projects/{project_id}/branches/{branch_id}` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project. + - `branch_id` (string, required): The unique identifier of the branch. + +Example Request: + +```bash +curl 'https://console.neon.tech/api/v2/projects/hidden-river-50598307/branches/br-super-wildflower-adniii9u' \ + -H 'accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" +``` + +Example Response: + +```json +{ + "branch": { + "id": "br-super-wildflower-adniii9u", + "project_id": "hidden-river-50598307", + "parent_id": "br-long-feather-adpbgzlx", + "parent_lsn": "0/1A33BC8", + "parent_timestamp": "2025-09-10T12:15:03Z", + "name": "development", + "current_state": "ready", + "state_changed_at": "2025-09-10T12:15:04Z", + "logical_size": 30842880, + "creation_source": "console", + "primary": false, + "default": false, + "protected": false, + "cpu_used_sec": 78, + "compute_time_seconds": 78, + "active_time_seconds": 312, + "written_data_bytes": 310120, + "data_transfer_bytes": 0, + "created_at": "2025-09-10T12:15:04Z", + "updated_at": "2025-09-10T12:35:33Z", + "created_by": { + "name": "", + "image": "" + }, + "init_source": "parent-data" + }, + "annotation": { + "object": { + "type": "console/branch", + "id": "br-super-wildflower-adniii9u" + }, + "value": { + "environment": "development" + }, + "created_at": "2025-09-10T12:15:04Z", + "updated_at": "2025-09-10T12:15:04Z" + } +} +``` + +### Update branch + +1. Action: Updates the properties of a specified branch, such as its name, protection status, or expiration time. +2. Endpoint: `PATCH /projects/{project_id}/branches/{branch_id}` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project. + - `branch_id` (string, required): The unique identifier of the branch to update. +4. Body Parameters: + `branch` (object, required): The container for the branch attributes to update. + - `name` (string, optional): A new name for the branch (max 256 characters). + - `protected` (boolean, optional): Set to `true` to protect the branch or `false` to unprotect it. + - `expires_at` (string or null, optional): Set a new RFC 3339 expiration timestamp or `null` to remove the expiration. + +Example: Change branch name: + +```bash +curl -X 'PATCH' \ + 'https://console.neon.tech/api/v2/projects/hidden-river-50598307/branches/br-damp-glitter-adqd4hk5' \ + -H 'accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" \ + -H 'Content-Type: application/json' \ + -d '{ + "branch": { + "name": "updated-branch-name" + } +}' +``` + +Example response: + +```json +{ + "branch": { + "id": "br-damp-glitter-adqd4hk5", + "project_id": "hidden-river-50598307", + "parent_id": "br-super-wildflower-adniii9u", + "parent_lsn": "0/1A7F730", + "parent_timestamp": "2025-09-10T12:15:05Z", + "name": "updated-branch-name", + "current_state": "ready", + "state_changed_at": "2025-09-10T16:45:52Z", + "logical_size": 30842880, + "creation_source": "console", + "primary": false, + "default": false, + "protected": false, + "cpu_used_sec": 68, + "compute_time_seconds": 68, + "active_time_seconds": 268, + "written_data_bytes": 0, + "data_transfer_bytes": 0, + "created_at": "2025-09-10T16:45:52Z", + "updated_at": "2025-09-10T16:55:30Z", + "created_by": { + "name": "", + "image": "" + }, + "init_source": "parent-data" + }, + "operations": [] +} +``` + +### Delete branch + +1. Action: Deletes the specified branch from a project. This action will also place all associated compute endpoints into an idle state, breaking any active client connections. +2. Endpoint: `DELETE /projects/{project_id}/branches/{branch_id}` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project. + - `branch_id` (string, required): The unique identifier of the branch to delete. +4. Constraints: + - You cannot delete a project's root or default branch. + - You cannot delete a branch that has child branches. You must delete all child branches first. + +Example Request: + +```bash +curl -X 'DELETE' \ + 'https://console.neon.tech/api/v2/projects/{project_id}/branches/{branch_id}' \ + -H 'accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" +``` + +### List branch endpoints + +1. Action: Retrieves a list of all compute endpoints that are associated with a specific branch. +2. Endpoint: `GET /projects/{project_id}/branches/{branch_id}/endpoints` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project. + - `branch_id` (string, required): The unique identifier of the branch whose endpoints you want to list. +4. A branch can have one `read_write` compute endpoint and multiple `read_only` endpoints. This method returns an array of all endpoints currently attached to the specified branch. + +Example Request: + +```bash +curl 'https://console.neon.tech/api/v2/projects/hidden-river-50598307/branches/br-super-wildflower-adniii9u/endpoints' \ + -H 'accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" +``` + +## Manage databases + +### Create database + +1. Action: Creates a new database within a specified branch. A branch can contain multiple databases. +2. Endpoint: `POST /projects/{project_id}/branches/{branch_id}/databases` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project. + - `branch_id` (string, required): The unique identifier of the branch where the database will be created. +4. Body Parameters: + `database` (object, required): The container for the new database's properties. + - `name` (string, required): The name for the new database. + - `owner_name` (string, required): The name of an existing role that will own the database. + +Example Request: + +```bash +curl 'https://console.neon.tech/api/v2/projects/hidden-river-50598307/branches/br-super-wildflower-adniii9u/databases' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" \ + -H 'Content-Type: application/json' \ + -d '{ + "database": { + "name": "my_new_app_db", + "owner_name": "app_owner_role" + } +}' +``` + +### List databases + +1. Action: Retrieves a list of all databases within a specified branch. +2. Endpoint: `GET /projects/{project_id}/branches/{branch_id}/databases` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project. + - `branch_id` (string, required): The unique identifier of the branch. + +Example Request: + +```bash +curl 'https://console.neon.tech/api/v2/projects/hidden-river-50598307/branches/br-super-wildflower-adniii9u/databases' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" +``` + +### Retrieve database details + +1. Action: Retrieves detailed information about a specific database within a branch. +2. Endpoint: `GET /projects/{project_id}/branches/{branch_id}/databases/{database_name}` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project. + - `branch_id` (string, required): The unique identifier of the branch. + - `database_name` (string, required): The name of the database. + +### Update database + +1. Action: Updates the properties of a specified database, such as its name or owner. +2. Endpoint: `PATCH /projects/{project_id}/branches/{branch_id}/databases/{database_name}` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project. + - `branch_id` (string, required): The unique identifier of the branch. + - `database_name` (string, required): The current name of the database to update. +4. Body Parameters: + `database` (object, required): The container for the database attributes to update. + - `name` (string, optional): A new name for the database. + - `owner_name` (string, optional): The name of a different existing role to become the new owner. + +### Delete database + +1. Action: Deletes the specified database from a branch. This action is permanent and cannot be undone. +2. Endpoint: `DELETE /projects/{project_id}/branches/{branch_id}/databases/{database_name}` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project. + - `branch_id` (string, required): The unique identifier of the branch. + - `database_name` (string, required): The name of the database to delete. + +## Manage roles + +### Create role + +1. Action: Creates a new Postgres role in a specified branch. This action may drop existing connections to the active compute endpoint. +2. Endpoint: `POST /projects/{project_id}/branches/{branch_id}/roles` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project. + - `branch_id` (string, required): The unique identifier of the branch where the role will be created. +4. Body Parameters: + `role` (object, required): The container for the new role's properties. + - `name` (string, required): The name for the new role. Cannot exceed 63 bytes in length. + - `no_login` (boolean, optional): If `true`, creates a role that cannot be used to log in. Defaults to `false`. + +Example Request: + +```bash +curl 'https://console.neon.tech/api/v2/projects/hidden-river-50598307/branches/br-super-wildflower-adniii9u/roles' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" \ + -H 'Content-Type: application/json' \ + -d '{ + "role": { + "name": "new_app_user" + } +}' +``` + +### List roles + +1. Action: Retrieves a list of all Postgres roles from the specified branch. +2. Endpoint: `GET /projects/{project_id}/branches/{branch_id}/roles` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project. + - `branch_id` (string, required): The unique identifier of the branch. + +### Retrieve role details + +1. Action: Retrieves detailed information about a specific Postgres role within a branch. +2. Endpoint: `GET /projects/{project_id}/branches/{branch_id}/roles/{role_name}` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project. + - `branch_id` (string, required): The unique identifier of the branch. + - `role_name` (string, required): The name of the role. + +### Delete role + +1. Action: Deletes the specified Postgres role from the branch. This action is permanent. +2. Endpoint: `DELETE /projects/{project_id}/branches/{branch_id}/roles/{role_name}` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project. + - `branch_id` (string, required): The unique identifier of the branch. + - `role_name` (string, required): The name of the role to delete. diff --git a/.agents/skills/neon-postgres/references/neon-rest-api/endpoints.md b/.agents/skills/neon-postgres/references/neon-rest-api/endpoints.md new file mode 100644 index 00000000..c1c79203 --- /dev/null +++ b/.agents/skills/neon-postgres/references/neon-rest-api/endpoints.md @@ -0,0 +1,211 @@ +## Overview + +This section provides rules for managing compute endpoints associated with branches in a project. Compute endpoints are Neon compute instances that allow you to connect to and interact with your databases. + +## Manage compute endpoints + +### Create compute endpoint + +1. Action: Creates a new compute endpoint (a Neon compute instance) and associates it with a specified branch. +2. Endpoint: `POST /projects/{project_id}/endpoints` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project. +4. Body Parameters: + `endpoint` (object, required): The container for the new endpoint's properties. + - `branch_id` (string, required): The ID of the branch to associate the endpoint with. + - `type` (string, required): The endpoint type. A branch can have only one `read_write` endpoint but multiple `read_only` endpoints. Allowed values: `read_write`, `read_only`. + - `region_id` (string, optional): The region where the endpoint will be created. Must match the project's region. + - `autoscaling_limit_min_cu` (number, optional): The minimum number of Compute Units (CU). Minimum `0.25`. + - `autoscaling_limit_max_cu` (number, optional): The maximum number of Compute Units (CU). Minimum `0.25`. + - `provisioner` (string, optional): The compute provisioner. Specify `k8s-neonvm` to enable Autoscaling. Allowed values: `k8s-pod`, `k8s-neonvm`. + - `suspend_timeout_seconds` (integer, optional): Duration of inactivity in seconds before suspending the compute. Ranges from -1 (never suspend) to 604800 (1 week). + - `disabled` (boolean, optional): If `true`, restricts connections to the endpoint. + +Example Request: + +```bash +curl 'https://console.neon.tech/api/v2/projects/hidden-river-50598307/endpoints' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" \ + -H 'Content-Type: application/json' \ + -d '{ + "endpoint": { + "branch_id": "br-your-branch-id", + "type": "read_only" + } +}' +``` + +Example Response: + +```json +{ + "endpoint": { + "host": "ep-proud-mud-adwmnxz4.c-2.us-east-1.aws.neon.tech", + "id": "ep-proud-mud-adwmnxz4", + "project_id": "hidden-river-50598307", + "branch_id": "br-super-wildflower-adniii9u", + "autoscaling_limit_min_cu": 0.25, + "autoscaling_limit_max_cu": 2, + "region_id": "aws-us-east-1", + "type": "read_only", + "current_state": "init", + "pending_state": "active", + "settings": {}, + "pooler_enabled": false, + "pooler_mode": "transaction", + "disabled": false, + "passwordless_access": true, + "creation_source": "console", + "created_at": "2025-09-11T06:25:12Z", + "updated_at": "2025-09-11T06:25:12Z", + "proxy_host": "c-2.us-east-1.aws.neon.tech", + "suspend_timeout_seconds": 0, + "provisioner": "k8s-neonvm" + }, + "operations": [ + { + "id": "4d10642f-5212-4517-ad60-afd28c9096e2", + "project_id": "hidden-river-50598307", + "branch_id": "br-super-wildflower-adniii9u", + "endpoint_id": "ep-proud-mud-adwmnxz4", + "action": "start_compute", + "status": "running", + "failures_count": 0, + "created_at": "2025-09-11T06:25:12Z", + "updated_at": "2025-09-11T06:25:12Z", + "total_duration_ms": 0 + } + ] +} +``` + +### List compute endpoints + +1. Action: Retrieves a list of all compute endpoints for the specified project. +2. Endpoint: `GET /projects/{project_id}/endpoints` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project. + +Example Request: + +```bash +curl 'https://console.neon.tech/api/v2/projects/hidden-river-50598307/endpoints' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" +``` + +### Retrieve compute endpoint details + +1. Action: Retrieves detailed information about a specific compute endpoint, including its configuration (e.g., autoscaling limits), current state (`active` or `idle`), and associated branch ID. +2. Endpoint: `GET /projects/{project_id}/endpoints/{endpoint_id}` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project. + - `endpoint_id` (string, required): The unique identifier of the compute endpoint). + +Example Request: + +```bash +curl 'https://console.neon.tech/api/v2/projects/hidden-river-50598307/endpoints/ep-proud-mud-adwmnxz4' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" +``` + +### Update compute endpoint + +1. Action: Updates the configuration of a specified compute endpoint. +2. Endpoint: `PATCH /projects/{project_id}/endpoints/{endpoint_id}` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project. + - `endpoint_id` (string, required): The unique identifier of the compute endpoint. +4. Body Parameters: + `endpoint` (object, required): The container for the endpoint attributes to update. + - `autoscaling_limit_min_cu` (number, optional): A new minimum number of Compute Units (CU). + - `autoscaling_limit_max_cu` (number, optional): A new maximum number of Compute Units (CU). + - `suspend_timeout_seconds` (integer, optional): A new inactivity period in seconds before suspension. + - `disabled` (boolean, optional): Set to `true` to disable connections or `false` to enable them. + - `provisioner` (string, optional): Change the compute provisioner. + +Example: Update autoscaling limits + +```bash +curl -X 'PATCH' \ + 'https://console.neon.tech/api/v2/projects/hidden-river-50598307/endpoints/ep-proud-mud-adwmnxz4' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" \ + -H 'Content-Type: application/json' \ + -d '{ + "endpoint": { + "autoscaling_limit_min_cu": 0.5, + "autoscaling_limit_max_cu": 1 + } +}' +``` + +### Delete compute endpoint + +1. Action: Deletes the specified compute endpoint. This action drops any existing network connections to the endpoint. +2. Endpoint: `DELETE /projects/{project_id}/endpoints/{endpoint_id}` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project. + - `endpoint_id` (string, required): The unique identifier of the compute endpoint to delete. + +Example Request: + +```bash +curl -X 'DELETE' \ + 'https://console.neon.tech/api/v2/projects/hidden-river-50598307/endpoints/ep-proud-mud-adwmnxz4' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" +``` + +### Start compute endpoint + +1. Action: Manually starts a compute endpoint that is currently in an `idle` state. The endpoint is ready for connections once the start operation completes successfully. +2. Endpoint: `POST /projects/{project_id}/endpoints/{endpoint_id}/start` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project. + - `endpoint_id` (string, required): The unique identifier of the compute endpoint. + +Example Request: + +```bash +curl -X 'POST' \ + 'https://console.neon.tech/api/v2/projects/hidden-river-50598307/endpoints/ep-ancient-brook-ad5ea04d/start' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" +``` + +### Suspend compute endpoint + +1. Action: Manually suspends an `active` compute endpoint, forcing it into an `idle` state. This will immediately drop any active connections to the endpoint. +2. Endpoint: `POST /projects/{project_id}/endpoints/{endpoint_id}/suspend` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project. + - `endpoint_id` (string, required): The unique identifier of the compute endpoint. + +Example Request: + +```bash +curl -X 'POST' \ + 'https://console.neon.tech/api/v2/projects/hidden-river-50598307/endpoints/ep-ancient-brook-ad5ea04d/suspend' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" +``` + +### Restart compute endpoint + +1. Action: Restarts the specified compute endpoint. This involves an immediate suspend operation followed by a start operation. This is useful for applying configuration changes or refreshing the compute instance. All active connections will be dropped. +2. Endpoint: `POST /projects/{project_id}/endpoints/{endpoint_id}/restart` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project. + - `endpoint_id` (string, required): The unique identifier of the compute endpoint. + +Example Request: + +```bash +curl -X 'POST' \ + 'https://console.neon.tech/api/v2/projects/hidden-river-50598307/endpoints/ep-ancient-brook-ad5ea04d/restart' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" +``` diff --git a/.agents/skills/neon-postgres/references/neon-rest-api/guidelines.md b/.agents/skills/neon-postgres/references/neon-rest-api/guidelines.md new file mode 100644 index 00000000..e3f044f0 --- /dev/null +++ b/.agents/skills/neon-postgres/references/neon-rest-api/guidelines.md @@ -0,0 +1,67 @@ +## Overview + +This document provides a comprehensive set of rules and guidelines for an AI agent to interact with the Neon API. The Neon API is a RESTful service that allows for programmatic management of all Neon resources. Adherence to these rules ensures correct, efficient, and safe API usage. + +### General API guidelines + +All Neon API requests must be made to the following base URL: + +``` +https://console.neon.tech/api/v2/ +``` + +To construct a full request URL, append the specific endpoint path to this base URL. + +### Authentication + +- All API requests must be authenticated using a Neon API key. +- The API key must be included in the `Authorization` header using the `Bearer` authentication scheme. +- The header should be formatted as: `Authorization: Bearer $NEON_API_KEY`, where `$NEON_API_KEY` is a valid Neon API key. +- A request without a valid `Authorization` header will fail with a `401 Unauthorized` status code. + +### API rate limiting + +- Neon limits API requests to 700 requests per minute (approximately 11 per second). +- Bursts of up to 40 requests per second per route are permitted. +- If the rate limit is exceeded, the API will respond with an `HTTP 429 Too Many Requests` error. +- Your application logic must handle `429` errors and implement a retry strategy with appropriate backoff. + +### Neon Core Concepts + +To effectively use the Neon Python SDK, it's essential to understand the hierarchy and purpose of its core resources. The following table provides a high-level overview of each concept. + +| Concept | Description | Analogy/Purpose | Key Relationship | +| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| Organization | The highest-level container, managing billing, users, and multiple projects. | A GitHub Organization or a company's cloud account. | Contains one or more Projects. | +| Project | The primary container that contains all related database resources for a single application or service. | A Git repository or a top-level folder for an application. | Lives within an Organization (or a personal account). Contains Branches. | +| Branch | A lightweight, copy-on-write clone of a database's state at a specific point in time. | A `git branch`. Used for isolated development, testing, staging, or previews without duplicating storage costs. | Belongs to a Project. Contains its own set of Databases and Roles, cloned from its parent. | +| Compute Endpoint | The actual running PostgreSQL instance that you connect to. It provides the CPU and RAM for processing queries. | The "server" or "engine" for your database. It can be started, suspended (scaled to zero), and resized. | Is attached to a single Branch. Your connection string points to a Compute Endpoint's hostname. | +| Database | A logical container for your data (tables, schemas, views) within a branch. It follows standard PostgreSQL conventions. | A single database within a PostgreSQL server instance. | Exists within a Branch. A branch can have multiple databases. | +| Role | A PostgreSQL role used for authentication (logging in) and authorization (permissions to access data). | A database user account with a username and password. | Belongs to a Branch. Roles from a parent branch are copied to child branches upon creation. | +| API Key | A secret token used to authenticate requests to the Neon API. Keys have different scopes (Personal, Organization, Project-scoped). | A password for programmatic access, allowing you to manage all other Neon resources. | Authenticates actions on Organizations, Projects, Branches, etc. | +| Operation | An asynchronous action performed by the Neon control plane, such as creating a branch or starting a compute. | A background job or task. Its status can be polled to know when an action is complete. | Associated with a Project and often a specific Branch or Endpoint. Essential for scripting API calls. | + +### Understanding API key types + +When performing actions via the API, you must select the correct type of API key based on the required scope and permissions. There are three types: + +1. Personal API Key + +- Scope: Accesses all projects that the user who created the key is a member of. +- Permissions: The key has the same permissions as its owner. If the user's access is revoked from an organization, the key loses access too. +- Best For: Individual use, scripting, and tasks tied to a specific user's permissions. +- Created By: Any user. + +2. Organization API Key + +- Scope: Accesses all projects and resources within an entire organization. +- Permissions: Has admin-level access across the organization, independent of any single user. It remains valid even if the creator leaves the organization. +- Best For: CI/CD pipelines, organization-wide automation, and service accounts that need broad access. +- Created By: Organization administrators only. + +3. Project-scoped API Key + +- Scope: Access is strictly limited to a single, specified project. +- Permissions: Cannot perform organization-level actions (like creating new projects) or delete the project it is scoped to. This is the most secure and limited key type. +- Best For: Project-specific integrations, third-party services, or automation that should be isolated to one project. +- Created By: Any organization member. diff --git a/.agents/skills/neon-postgres/references/neon-rest-api/keys.md b/.agents/skills/neon-postgres/references/neon-rest-api/keys.md new file mode 100644 index 00000000..56da43f7 --- /dev/null +++ b/.agents/skills/neon-postgres/references/neon-rest-api/keys.md @@ -0,0 +1,92 @@ +## Overview + +This document outlines the rules for managing Neon API keys programmatically. It covers listing existing keys, creating new keys, and revoking keys. + +### Important note on creating API keys + +To create new API keys using the API, you must already possess a valid Personal API Key. The first key must be created from the Neon Console. You can ask the user to create one for you if you do not have one. + +### List API keys + +- Endpoint: `GET /api_keys` +- Authorization: Use a Personal API Key. + +Example request: + +```bash +curl "https://console.neon.tech/api/v2/api_keys" \ + -H "Authorization: Bearer $PERSONAL_API_KEY" +``` + +Example response: + +```json +[ + { + "id": 2291506, + "name": "my-personal-key", + "created_at": "2025-09-10T09:44:04Z", + "created_by": { + "id": "487de658-08ba-4363-b387-86d18b9ad1c8", + "name": "", + "image": "" + }, + "last_used_at": "2025-09-10T09:44:09Z", + "last_used_from_addr": "49.43.218.132,34.211.200.85" + } +] +``` + +### Create an API key + +- Endpoint: `POST /api_keys` +- Authorization: Use a Personal API Key. +- Body: Must include a `key_name`. + +Example request: + +```bash +curl https://console.neon.tech/api/v2/api_keys \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $PERSONAL_API_KEY" \ + -d '{"key_name": "my-new-key"}' +``` + +Example response: + +```json +{ + "id": 2291515, + "key": "napi_9tlr13774gizljemrr133j5koy3bmsphj8iu38mh0yjl9q4r1b0jy2wuhhuxouzr", + "name": "my-new-key", + "created_at": "2025-09-10T09:47:59Z", + "created_by": "487de658-08ba-4363-b387-86d18b9ad1c8" +} +``` + +### Revoke an API key + +- Endpoint: `DELETE /api_keys/{key_id}` +- Authorization: Use a Personal API Key. + +Example request: + +```bash +curl -X DELETE \ + 'https://console.neon.tech/api/v2/api_keys/2291515' \ + -H "Authorization: Bearer $PERSONAL_API_KEY" +``` + +Example response: + +```json +{ + "id": 2291515, + "name": "mynewkey", + "created_at": "2025-09-10T09:47:59Z", + "created_by": "487de658-08ba-4363-b387-86d18b9ad1c8", + "last_used_at": "2025-09-10T09:53:01Z", + "last_used_from_addr": "2405:201:c01f:7013:d962:2b4f:2740:9750", + "revoked": true +} +``` diff --git a/.agents/skills/neon-postgres/references/neon-rest-api/operations.md b/.agents/skills/neon-postgres/references/neon-rest-api/operations.md new file mode 100644 index 00000000..2fe3d538 --- /dev/null +++ b/.agents/skills/neon-postgres/references/neon-rest-api/operations.md @@ -0,0 +1,146 @@ +## Overview + +This document outlines the rules for managing and monitoring long-running operations in Neon, including branch creation and compute management. + +## Operations + +An operation is an action performed by the Neon Control Plane (e.g., `create_branch`, `start_compute`). When using the API programmatically, it is crucial to monitor the status of long-running operations to ensure one has completed before starting another that depends on it. Operations older than 6 months may be deleted from Neon's systems. + +### List operations + +1. Action: Retrieves a list of operations for the specified Neon project. The number of operations can be large, so pagination is recommended. +2. Endpoint: `GET /projects/{project_id}/operations` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project whose operations you want to list. +4. Query Parameters: + - `limit` (integer, optional): The number of operations to return in the response. Must be between 1 and 1000. + - `cursor` (string, optional): The cursor value from a previous response to fetch the next page of operations. +5. Procedure: + - Make an initial request with a `limit` to get the first page of results. + - The response will contain a `pagination.cursor` value. + - To get the next page, make a subsequent request including both the `limit` and the `cursor` from the previous response. + +Example request + +```bash +curl 'https://console.neon.tech/api/v2/projects/hidden-river-50598307/operations' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" +``` + +Example response + +```json +{ + "operations": [ + { + "id": "639f7f73-0b76-4749-a767-2d3c627ca5a6", + "project_id": "hidden-river-50598307", + "branch_id": "br-long-feather-adpbgzlx", + "endpoint_id": "ep-round-morning-adtpn2oc", + "action": "apply_config", + "status": "finished", + "failures_count": 0, + "created_at": "2025-09-10T12:15:23Z", + "updated_at": "2025-09-10T12:15:23Z", + "total_duration_ms": 87 + }, + { + "id": "b5a7882b-a5b3-4292-ad27-bffe733feae4", + "project_id": "hidden-river-50598307", + "branch_id": "br-super-wildflower-adniii9u", + "endpoint_id": "ep-ancient-brook-ad5ea04d", + "action": "apply_config", + "status": "finished", + "failures_count": 0, + "created_at": "2025-09-10T12:15:23Z", + "updated_at": "2025-09-10T12:15:23Z", + "total_duration_ms": 49 + }, + { + "id": "36a1cba0-97f1-476d-af53-d9e0d3a3606d", + "project_id": "hidden-river-50598307", + "branch_id": "br-super-wildflower-adniii9u", + "endpoint_id": "ep-ancient-brook-ad5ea04d", + "action": "start_compute", + "status": "finished", + "failures_count": 0, + "created_at": "2025-09-10T12:15:04Z", + "updated_at": "2025-09-10T12:15:05Z", + "total_duration_ms": 913 + }, + { + "id": "409c35ef-cbc3-4f1b-a4ca-f2de319f5360", + "project_id": "hidden-river-50598307", + "branch_id": "br-super-wildflower-adniii9u", + "action": "create_branch", + "status": "finished", + "failures_count": 0, + "created_at": "2025-09-10T12:15:04Z", + "updated_at": "2025-09-10T12:15:04Z", + "total_duration_ms": 136 + }, + { + "id": "274e240f-e2fb-4719-b796-c1ab7c4ae91c", + "project_id": "hidden-river-50598307", + "branch_id": "br-long-feather-adpbgzlx", + "endpoint_id": "ep-round-morning-adtpn2oc", + "action": "start_compute", + "status": "finished", + "failures_count": 0, + "created_at": "2025-09-10T12:14:58Z", + "updated_at": "2025-09-10T12:15:03Z", + "total_duration_ms": 4843 + }, + { + "id": "22ef6fbd-21c5-4cdb-9825-b0f9afddbb0d", + "project_id": "hidden-river-50598307", + "branch_id": "br-long-feather-adpbgzlx", + "action": "create_timeline", + "status": "finished", + "failures_count": 0, + "created_at": "2025-09-10T12:14:58Z", + "updated_at": "2025-09-10T12:15:01Z", + "total_duration_ms": 3096 + } + ], + "pagination": { + "cursor": "2025-09-10T12:14:58.848485Z" + } +} +``` + +### Retrieve operation details + +1. Action: Retrieves the details and status of a single, specified operation. The `operation_id` is found in the response body of the initial API call that initiated it, or by listing operations. +2. Endpoint: `GET /projects/{project_id}/operations/{operation_id}` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project where the operation occurred. + - `operation_id` (UUID, required): The unique identifier of the operation. This ID is returned in the response body of the API call that initiated the operation. + +Example request: + +```bash +curl 'https://console.neon.tech/api/v2/projects/hidden-river-50598307/operations/274e240f-e2fb-4719-b796-c1ab7c4ae91c' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" +``` + +Example response: + +```json +{ + "operation": { + "id": "274e240f-e2fb-4719-b796-c1ab7c4ae91c", + "project_id": "hidden-river-50598307", + "branch_id": "br-long-feather-adpbgzlx", + "endpoint_id": "ep-round-morning-adtpn2oc", + "action": "start_compute", + "status": "finished", + "failures_count": 0, + "created_at": "2025-09-10T12:14:58Z", + "updated_at": "2025-09-10T12:15:03Z", + "total_duration_ms": 4843 + } +} +``` diff --git a/.agents/skills/neon-postgres/references/neon-rest-api/organizations.md b/.agents/skills/neon-postgres/references/neon-rest-api/organizations.md new file mode 100644 index 00000000..ecd1b3d7 --- /dev/null +++ b/.agents/skills/neon-postgres/references/neon-rest-api/organizations.md @@ -0,0 +1,199 @@ +## Overview + +This section provides rules for managing organizations, their members, invitations, and organization API keys. Organizations allow multiple users to collaborate on projects and share resources within Neon. + +## Manage organizations + +### Retrieve organization details + +1. Action: Retrieves detailed information about a specific organization. +2. Endpoint: `GET /organizations/{org_id}` +3. Path Parameters: + - `org_id` (string, required): The unique identifier of the organization. + +Example Request: + +```bash +curl 'https://console.neon.tech/api/v2/organizations/{org_id}' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" +``` + +### List organization members + +1. Action: Retrieves a list of all members belonging to the specified organization. +2. Endpoint: `GET /organizations/{org_id}/members` +3. Path Parameters: + - `org_id` (string, required): The unique identifier of the organization. + +Example Request: + +```bash +curl 'https://console.neon.tech/api/v2/organizations/{org_id}/members' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" +``` + +### Retrieve organization member details + +1. Action: Retrieves information about a specific member of an organization. +2. Endpoint: `GET /organizations/{org_id}/members/{member_id}` +3. Path Parameters: + - `org_id` (string, required): The unique identifier of the organization. + - `member_id` (UUID, required): The unique identifier of the organization member. + +Example Request: + +```bash +curl 'https://console.neon.tech/api/v2/organizations/{org_id}/members/{member_id}' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" +``` + +### Update role for organization member + +1. Action: Updates the role of a specified member within an organization. +2. Prerequisite: This action can only be performed by an organization `admin`. +3. Endpoint: `PATCH /organizations/{org_id}/members/{member_id}` +4. Path Parameters: + - `org_id` (string, required): The unique identifier of the organization. + - `member_id` (UUID, required): The unique identifier of the organization member. +5. Body Parameters: + - `role` (string, required): The new role for the member. Allowed values: `admin`, `member`. + +Example: Change a member's role to admin + +```bash +curl -X 'PATCH' \ + 'https://console.neon.tech/api/v2/organizations/{org_id}/members/{member_id}' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" \ + -H 'Content-Type: application/json' \ + -d '{"role": "admin"}' +``` + +### Remove member from organization + +1. Action: Removes a specified member from an organization. +2. Prerequisites: + - This action can only be performed by an organization `admin`. + - An admin cannot be removed if they are the only admin left in the organization. +3. Endpoint: `DELETE /organizations/{org_id}/members/{member_id}` +4. Path Parameters: + - `org_id` (string, required): The unique identifier of the organization. + - `member_id` (UUID, required): The unique identifier of the organization member to remove. + +Example Request: + +```bash +curl -X 'DELETE' \ + 'https://console.neon.tech/api/v2/organizations/{org_id}/members/{member_id}' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" +``` + +### Create organization invitations + +1. Action: Creates and sends one or more email invitations for users to join a specific organization. +2. Endpoint: `POST /organizations/{org_id}/invitations` +3. Path Parameters: + - `org_id` (string, required): The unique identifier of the organization. +4. Body Parameters: + `invitations` (array of objects, required): A list of invitations to create. + - `email` (string, required): The email address of the user to invite. + - `role` (string, required): The role the invited user will have. Allowed values: `admin`, `member`. + +Example: Invite two users with different roles + +```bash +curl -X 'POST' \ + 'https://console.neon.tech/api/v2/organizations/{org_id}/invitations' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" \ + -H 'Content-Type: application/json' \ + -d '{ + "invitations": [ + { + "email": "developer@example.com", + "role": "member" + }, + { + "email": "manager@example.com", + "role": "admin" + } + ] +}' +``` + +### List organization invitations + +1. Action: Retrieves information about outstanding invitations for the specified organization. +2. Endpoint: `GET /organizations/{org_id}/invitations` +3. Path Parameters: + - `org_id` (string, required): The unique identifier of the organization. + +Example Request: + +```bash +curl 'https://console.neon.tech/api/v2/organizations/{org_id}/invitations' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" +``` + +### Create organization API key + +1. Action: Creates a new API key for the specified organization. The key can be scoped to the entire organization or limited to a single project within it. +2. Endpoint: `POST /organizations/{org_id}/api_keys` +3. Path Parameters: + - `org_id` (string, required): The unique identifier of the organization. +4. Body Parameters: + - `key_name` (string, required): A user-specified name for the API key (max 64 characters). + - `project_id` (string, optional): If provided, the API key's access will be restricted to only this project. +5. Authorization: Use a Personal API Key of an organization `admin` to create organization API keys. + +Example: Create a project-scoped API key + +```bash +curl -X 'POST' \ + 'https://console.neon.tech/api/v2/organizations/{org_id}/api_keys' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $PERSONAL_API_KEY_OF_ADMIN" \ + -H 'Content-Type: application/json' \ + -d '{ + "key_name": "ci-pipeline-key-for-project-x", + "project_id": "project-id-123" +}' +``` + +### List organization API keys + +1. Action: Retrieves a list of all API keys created for the specified organization. +2. Endpoint: `GET /organizations/{org_id}/api_keys` +3. Note: The response includes metadata about the keys (like `id` and `name`) but does not include the secret key tokens themselves. Tokens are only visible upon creation. +4. Path Parameters: + - `org_id` (string, required): The unique identifier of the organization. + +Example Request: + +```bash +curl 'https://console.neon.tech/api/v2/organizations/{org_id}/api_keys' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" +``` + +### Revoke organization API key + +1. Action: Permanently revokes the specified organization API key. +2. Endpoint: `DELETE /organizations/{org_id}/api_keys/{key_id}` +3. Path Parameters: + - `org_id` (string, required): The unique identifier of the organization. + - `key_id` (integer, required): The unique identifier of the API key to revoke. You can obtain this ID by listing the organization's API keys. + +Example Request: + +```bash +curl -X 'DELETE' \ + 'https://console.neon.tech/api/v2/organizations/{org_id}/api_keys/{key_id}' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" +``` diff --git a/.agents/skills/neon-postgres/references/neon-rest-api/projects.md b/.agents/skills/neon-postgres/references/neon-rest-api/projects.md new file mode 100644 index 00000000..fb3a9b43 --- /dev/null +++ b/.agents/skills/neon-postgres/references/neon-rest-api/projects.md @@ -0,0 +1,576 @@ +## Overview + +This document outlines the rules for managing Neon projects programmatically. It covers creation, retrieval, updates, and deletion. + +## Manage projects + +### List projects + +1. Action: Retrieves a list of all projects accessible to the account associated with the API key. This is the primary method for obtaining `project_id` values required for other API calls. +2. Endpoint: `GET /projects` +3. Query Parameters: + - `limit` (optional, integer, default: 10): Specifies the number of projects to return, from 1 to 400. + - `cursor` (optional, string): Used for pagination. Provide the `cursor` value from a previous response to fetch the next set of projects. + - `search` (optional, string): Filters projects by a partial match on the project `name` or `id`. + - `org_id` (optional, string): Filters projects by a specific organization ID. +4. When iterating through all projects, use a combination of the `limit` and `cursor` parameters to handle pagination correctly. + +Example request: + +```bash +# Retrieve the first 10 projects +curl 'https://console.neon.tech/api/v2/projects?limit=10' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" +``` + +Example response: + +```json +{ + "projects": [ + { + "id": "old-fire-32990194", + "platform_id": "aws", + "region_id": "aws-ap-southeast-1", + "name": "old-fire-32990194", + "provisioner": "k8s-neonvm", + "default_endpoint_settings": { + "autoscaling_limit_min_cu": 0.25, + "autoscaling_limit_max_cu": 2, + "suspend_timeout_seconds": 0 + }, + "settings": { + "allowed_ips": { + "ips": [], + "protected_branches_only": false + }, + "enable_logical_replication": false, + "maintenance_window": { + "weekdays": [5], + "start_time": "19:00", + "end_time": "20:00" + }, + "block_public_connections": false, + "block_vpc_connections": false, + "hipaa": false + }, + "pg_version": 17, + "proxy_host": "ap-southeast-1.aws.neon.tech", + "branch_logical_size_limit": 512, + "branch_logical_size_limit_bytes": 536870912, + "store_passwords": true, + "active_time": 0, + "cpu_used_sec": 0, + "creation_source": "console", + "created_at": "2025-09-10T06:58:33Z", + "updated_at": "2025-09-10T06:58:39Z", + "synthetic_storage_size": 0, + "quota_reset_at": "2025-10-01T00:00:00Z", + "owner_id": "org-royal-sun-91776391", + "compute_last_active_at": "2025-09-10T06:58:38Z", + "org_id": "org-royal-sun-91776391", + "history_retention_seconds": 86400 + } + ], + "pagination": { + "cursor": "old-fire-32990194" + }, + "applications": {}, + "integrations": {} +} +``` + +### Create project + +1. Action: Creates a new Neon project. You can specify a wide range of settings at creation time, including the region, Postgres version, default branch and compute configurations, and security settings. +2. Endpoint: `POST /projects` +3. Body Parameters: The request body must contain a top-level `project` object with the following nested attributes: + + `project` (object, required): The main container for all project settings. + - `name` (string, optional): A descriptive name for the project (1-256 characters). If omitted, the project name will be identical to its generated ID. + - `pg_version` (integer, optional): The major Postgres version. Defaults to `17`. Supported versions: 14, 15, 16, 17, 18. + - `region_id` (string, optional): The identifier for the region where the project will be created (e.g., `aws-us-east-1`). + - `org_id` (string, optional): The ID of an organization to which the project will belong. Required if using an Organization API key. + - `store_passwords` (boolean, optional): Whether to store role passwords in Neon. Storing passwords is required for features like the SQL Editor and integrations. + - `history_retention_seconds` (integer, optional): The duration in seconds (0 to 2,592,000) to retain project history for features like Point-in-Time Restore. Defaults to 86400 (1 day). + - `provisioner` (string, optional): The compute provisioner. Specify `k8s-neonvm` to enable Autoscaling. Allowed values: `k8s-pod`, `k8s-neonvm`. + - `default_endpoint_settings` (object, optional): Default settings for new compute endpoints created in this project. + - `autoscaling_limit_min_cu` (number, optional): The minimum number of Compute Units (CU). Minimum value is `0.25`. + - `autoscaling_limit_max_cu` (number, optional): The maximum number of Compute Units (CU). Minimum value is `0.25`. + - `suspend_timeout_seconds` (integer, optional): Duration of inactivity in seconds before a compute is suspended. Ranges from -1 (never suspend) to 604800 (1 week). A value of `0` uses the default of 300 seconds (5 minutes). + - `settings` (object, optional): Project-wide settings. + - `quota` (object, optional): Per-project consumption quotas. A zero or empty value means "unlimited". + - `active_time_seconds` (integer, optional): Wall-clock time allowance for active computes. + - `compute_time_seconds` (integer, optional): CPU seconds allowance. + - `written_data_bytes` (integer, optional): Data written allowance. + - `data_transfer_bytes` (integer, optional): Data transferred allowance. + - `logical_size_bytes` (integer, optional): Logical data size limit per branch. + - `allowed_ips` (object, optional): Configures the IP Allowlist. + - `ips` (array of strings, optional): A list of allowed IP addresses or CIDR ranges. + - `protected_branches_only` (boolean, optional): If `true`, the IP allowlist applies only to protected branches. + - `enable_logical_replication` (boolean, optional): Sets `wal_level=logical`. + - `maintenance_window` (object, optional): The time period for scheduled maintenance. + - `weekdays` (array of integers, required if `maintenance_window` is set): Days of the week (1=Monday, 7=Sunday). + - `start_time` (string, required if `maintenance_window` is set): Start time in "HH:MM" UTC format. + - `end_time` (string, required if `maintenance_window` is set): End time in "HH:MM" UTC format. + - `branch` (object, optional): Configuration for the project's default branch. + - `name` (string, optional): The name for the default branch. Defaults to `main`. + - `role_name` (string, optional): The name for the default role. Defaults to `{database_name}_owner`. + - `database_name` (string, optional): The name for the default database. Defaults to `neondb`. + +Example request + +```bash +curl -X POST 'https://console.neon.tech/api/v2/projects' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" \ + -H 'Content-Type: application/json' \ + -d '{ + "project": { + "name": "my-new-api-project", + "pg_version": 17 + } +}' +``` + +Example response + +```json +{ + "project": { + "data_storage_bytes_hour": 0, + "data_transfer_bytes": 0, + "written_data_bytes": 0, + "compute_time_seconds": 0, + "active_time_seconds": 0, + "cpu_used_sec": 0, + "id": "sparkling-hill-99143322", + "platform_id": "aws", + "region_id": "aws-us-west-2", + "name": "my-new-api-project", + "provisioner": "k8s-neonvm", + "default_endpoint_settings": { + "autoscaling_limit_min_cu": 0.25, + "autoscaling_limit_max_cu": 0.25, + "suspend_timeout_seconds": 0 + }, + "settings": { + "allowed_ips": { + "ips": [], + "protected_branches_only": false + }, + "enable_logical_replication": false, + "maintenance_window": { + "weekdays": [5], + "start_time": "07:00", + "end_time": "08:00" + }, + "block_public_connections": false, + "block_vpc_connections": false, + "hipaa": false + }, + "pg_version": 17, + "proxy_host": "c-2.us-west-2.aws.neon.tech", + "branch_logical_size_limit": 512, + "branch_logical_size_limit_bytes": 536870912, + "store_passwords": true, + "creation_source": "console", + "history_retention_seconds": 86400, + "created_at": "2025-09-10T07:58:16Z", + "updated_at": "2025-09-10T07:58:16Z", + "consumption_period_start": "0001-01-01T00:00:00Z", + "consumption_period_end": "0001-01-01T00:00:00Z", + "owner_id": "org-royal-sun-91776391", + "org_id": "org-royal-sun-91776391" + }, + "connection_uris": [ + { + "connection_uri": "postgresql://neondb_owner:npg_N67FDMtGvJke@ep-round-unit-afbn7qv4.c-2.us-west-2.aws.neon.tech/neondb?sslmode=require", + "connection_parameters": { + "database": "neondb", + "password": "npg_N67FDMtGvJke", + "role": "neondb_owner", + "host": "ep-round-unit-afbn7qv4.c-2.us-west-2.aws.neon.tech", + "pooler_host": "ep-round-unit-afbn7qv4-pooler.c-2.us-west-2.aws.neon.tech" + } + } + ], + "roles": [ + { + "branch_id": "br-green-mode-afe3fl9y", + "name": "neondb_owner", + "password": "npg_N67FDMtGvJke", + "protected": false, + "created_at": "2025-09-10T07:58:16Z", + "updated_at": "2025-09-10T07:58:16Z" + } + ], + "databases": [ + { + "id": 6677853, + "branch_id": "br-green-mode-afe3fl9y", + "name": "neondb", + "owner_name": "neondb_owner", + "created_at": "2025-09-10T07:58:16Z", + "updated_at": "2025-09-10T07:58:16Z" + } + ], + "operations": [ + { + "id": "08b9367d-6918-4cd5-b4a6-41c8fd984b7e", + "project_id": "sparkling-hill-99143322", + "branch_id": "br-green-mode-afe3fl9y", + "action": "create_timeline", + "status": "running", + "failures_count": 0, + "created_at": "2025-09-10T07:58:16Z", + "updated_at": "2025-09-10T07:58:16Z", + "total_duration_ms": 0 + }, + { + "id": "c6917f04-5cd3-48a2-97c9-186b1d9729f0", + "project_id": "sparkling-hill-99143322", + "branch_id": "br-green-mode-afe3fl9y", + "endpoint_id": "ep-round-unit-afbn7qv4", + "action": "start_compute", + "status": "scheduling", + "failures_count": 0, + "created_at": "2025-09-10T07:58:16Z", + "updated_at": "2025-09-10T07:58:16Z", + "total_duration_ms": 0 + } + ], + "branch": { + "id": "br-green-mode-afe3fl9y", + "project_id": "sparkling-hill-99143322", + "name": "main", + "current_state": "init", + "pending_state": "ready", + "state_changed_at": "2025-09-10T07:58:16Z", + "creation_source": "console", + "primary": true, + "default": true, + "protected": false, + "cpu_used_sec": 0, + "compute_time_seconds": 0, + "active_time_seconds": 0, + "written_data_bytes": 0, + "data_transfer_bytes": 0, + "created_at": "2025-09-10T07:58:16Z", + "updated_at": "2025-09-10T07:58:16Z", + "init_source": "parent-data" + }, + "endpoints": [ + { + "host": "ep-round-unit-afbn7qv4.c-2.us-west-2.aws.neon.tech", + "id": "ep-round-unit-afbn7qv4", + "project_id": "sparkling-hill-99143322", + "branch_id": "br-green-mode-afe3fl9y", + "autoscaling_limit_min_cu": 0.25, + "autoscaling_limit_max_cu": 0.25, + "region_id": "aws-us-west-2", + "type": "read_write", + "current_state": "init", + "pending_state": "active", + "settings": {}, + "pooler_enabled": false, + "pooler_mode": "transaction", + "disabled": false, + "passwordless_access": true, + "creation_source": "console", + "created_at": "2025-09-10T07:58:16Z", + "updated_at": "2025-09-10T07:58:16Z", + "proxy_host": "c-2.us-west-2.aws.neon.tech", + "suspend_timeout_seconds": 0, + "provisioner": "k8s-neonvm" + } + ] +} +``` + +### Retrieve project details + +1. Action: Retrieves detailed information about a single, specific project. +2. Endpoint: `GET /projects/{project_id}` +3. Prerequisite: You must have the `project_id` of the project you wish to retrieve. +4. Path Parameters: + - `project_id` (required, string): The unique identifier of the project. + +Example request: + +```bash +curl 'https://console.neon.tech/api/v2/projects/sparkling-hill-99143322' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" +``` + +Example response + +```json +{ + "project": { + "data_storage_bytes_hour": 0, + "data_transfer_bytes": 0, + "written_data_bytes": 0, + "compute_time_seconds": 0, + "active_time_seconds": 0, + "cpu_used_sec": 0, + "id": "sparkling-hill-99143322", + "platform_id": "aws", + "region_id": "aws-us-west-2", + "name": "my-new-api-project", + "provisioner": "k8s-neonvm", + "default_endpoint_settings": { + "autoscaling_limit_min_cu": 0.25, + "autoscaling_limit_max_cu": 0.25, + "suspend_timeout_seconds": 0 + }, + "settings": { + "allowed_ips": { + "ips": [], + "protected_branches_only": false + }, + "enable_logical_replication": false, + "maintenance_window": { + "weekdays": [5], + "start_time": "07:00", + "end_time": "08:00" + }, + "block_public_connections": false, + "block_vpc_connections": false, + "hipaa": false + }, + "pg_version": 17, + "proxy_host": "c-2.us-west-2.aws.neon.tech", + "branch_logical_size_limit": 512, + "branch_logical_size_limit_bytes": 536870912, + "store_passwords": true, + "creation_source": "console", + "history_retention_seconds": 86400, + "created_at": "2025-09-10T07:58:16Z", + "updated_at": "2025-09-10T07:58:25Z", + "synthetic_storage_size": 0, + "consumption_period_start": "2025-09-10T06:58:15Z", + "consumption_period_end": "2025-10-01T00:00:00Z", + "owner_id": "org-royal-sun-91776391", + "owner": { + "email": "", + "name": "My Personal Account", + "branches_limit": 10, + "subscription_type": "free_v3" + }, + "compute_last_active_at": "2025-09-10T07:58:21Z", + "org_id": "org-royal-sun-91776391" + } +} +``` + +### Update a project + +1. Action: Updates the settings of a specified project. This endpoint is used to modify a wide range of project attributes after creation, such as its name, default compute settings, security policies, and maintenance schedules. +2. Endpoint: `PATCH /projects/{project_id}` +3. Path Parameters: + - `project_id` (string, required): The unique identifier of the project to update. +4. Body Parameters: The request body must contain a top-level `project` object with the attributes to be updated. + + `project` (object, required): The main container for the settings you want to modify. + - `name` (string, optional): A new descriptive name for the project. + - `history_retention_seconds` (integer, optional): The duration in seconds (0 to 2,592,000) to retain project history. + - `default_endpoint_settings` (object, optional): New default settings for compute endpoints created in this project. + - `autoscaling_limit_min_cu` (number, optional): The minimum number of Compute Units (CU). Minimum `0.25`. + - `autoscaling_limit_max_cu` (number, optional): The maximum number of Compute Units (CU). Minimum `0.25`. + - `suspend_timeout_seconds` (integer, optional): Duration of inactivity in seconds before a compute is suspended. Ranges from -1 (never suspend) to 604800 (1 week). A value of `0` uses the default of 300 seconds (5 minutes). + - `settings` (object, optional): Project-wide settings to update. + - `quota` (object, optional): Per-project consumption quotas. + - `active_time_seconds` (integer, optional): Wall-clock time allowance for active computes. + - `compute_time_seconds` (integer, optional): CPU seconds allowance. + - `written_data_bytes` (integer, optional): Data written allowance. + - `data_transfer_bytes` (integer, optional): Data transferred allowance. + - `logical_size_bytes` (integer, optional): Logical data size limit per branch. + - `allowed_ips` (object, optional): Modifies the IP Allowlist. + - `ips` (array of strings, optional): The new list of allowed IP addresses or CIDR ranges. + - `protected_branches_only` (boolean, optional): If `true`, the IP allowlist applies only to protected branches. + - `enable_logical_replication` (boolean, optional): Sets `wal_level=logical`. This is irreversible. + - `maintenance_window` (object, optional): The time period for scheduled maintenance. + - `weekdays` (array of integers, required if `maintenance_window` is set): Days of the week (1=Monday, 7=Sunday). + - `start_time` (string, required if `maintenance_window` is set): Start time in "HH:MM" UTC format. + - `end_time` (string, required if `maintenance_window` is set): End time in "HH:MM" UTC format. + - `block_public_connections` (boolean, optional): If `true`, disallows connections from the public internet. + - `block_vpc_connections` (boolean, optional): If `true`, disallows connections from VPC endpoints. + - `audit_log_level` (string, optional): Sets the audit log level. Allowed values: `base`, `extended`, `full`. + - `hipaa` (boolean, optional): Toggles HIPAA compliance settings. + - `preload_libraries` (object, optional): Libraries to preload into compute instances. + - `use_defaults` (boolean, optional): Toggles the use of default libraries. + - `enabled_libraries` (array of strings, optional): A list of specific libraries to enable. + +Example request + +```bash +curl -X PATCH 'https://console.neon.tech/api/v2/projects/sparkling-hill-99143322' \ + -H 'accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" \ + -H 'Content-Type: application/json' \ + -d '{ + "project": { + "name": "updated-project-name" + } +}' +``` + +Example response + +```json +{ + "project": { + "data_storage_bytes_hour": 0, + "data_transfer_bytes": 0, + "written_data_bytes": 29060360, + "compute_time_seconds": 79, + "active_time_seconds": 308, + "cpu_used_sec": 79, + "id": "sparkling-hill-99143322", + "platform_id": "aws", + "region_id": "aws-us-west-2", + "name": "updated-project-name", + "provisioner": "k8s-neonvm", + "default_endpoint_settings": { + "autoscaling_limit_min_cu": 0.25, + "autoscaling_limit_max_cu": 0.25, + "suspend_timeout_seconds": 0 + }, + "settings": { + "allowed_ips": { + "ips": [], + "protected_branches_only": false + }, + "enable_logical_replication": false, + "maintenance_window": { + "weekdays": [5], + "start_time": "07:00", + "end_time": "08:00" + }, + "block_public_connections": false, + "block_vpc_connections": false, + "hipaa": false + }, + "pg_version": 17, + "proxy_host": "c-2.us-west-2.aws.neon.tech", + "branch_logical_size_limit": 512, + "branch_logical_size_limit_bytes": 536870912, + "store_passwords": true, + "creation_source": "console", + "history_retention_seconds": 86400, + "created_at": "2025-09-10T07:58:16Z", + "updated_at": "2025-09-10T08:08:23Z", + "synthetic_storage_size": 0, + "consumption_period_start": "0001-01-01T00:00:00Z", + "consumption_period_end": "0001-01-01T00:00:00Z", + "owner_id": "org-royal-sun-91776391", + "compute_last_active_at": "2025-09-10T07:58:21Z" + }, + "operations": [] +} +``` + +### Delete project + +1. Action: Permanently deletes a project and all of its associated resources, including all branches, computes, databases, and roles. +2. Endpoint: `DELETE /projects/{project_id}` +3. Prerequisite: You must have the `project_id` of the project you wish to delete. +4. Warning: This is a destructive action that cannot be undone. It deletes all data, databases, and resources in the project. Proceed with extreme caution and confirm with the user before executing this operation. +5. Path Parameters: + - `project_id` (required, string): The unique identifier of the project to be deleted. + +Example request: + +```bash +curl -X 'DELETE' \ + 'https://console.neon.tech/api/v2/projects/sparkling-hill-99143322' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" +``` + +Example response: + +```json +{ + "project": { + "data_storage_bytes_hour": 0, + "data_transfer_bytes": 0, + "written_data_bytes": 29060360, + "compute_time_seconds": 79, + "active_time_seconds": 308, + "cpu_used_sec": 79, + "id": "sparkling-hill-99143322", + "platform_id": "aws", + "region_id": "aws-us-west-2", + "name": "updated-project-name", + "provisioner": "k8s-neonvm", + "default_endpoint_settings": { + "autoscaling_limit_min_cu": 0.25, + "autoscaling_limit_max_cu": 0.25, + "suspend_timeout_seconds": 0 + }, + "settings": { + "allowed_ips": { + "ips": [], + "protected_branches_only": false + }, + "enable_logical_replication": false, + "maintenance_window": { + "weekdays": [5], + "start_time": "07:00", + "end_time": "08:00" + }, + "block_public_connections": false, + "block_vpc_connections": false, + "hipaa": false + }, + "pg_version": 17, + "proxy_host": "c-2.us-west-2.aws.neon.tech", + "branch_logical_size_limit": 512, + "branch_logical_size_limit_bytes": 536870912, + "store_passwords": true, + "creation_source": "console", + "history_retention_seconds": 86400, + "created_at": "2025-09-10T07:58:16Z", + "updated_at": "2025-09-10T08:08:23Z", + "synthetic_storage_size": 0, + "consumption_period_start": "0001-01-01T00:00:00Z", + "consumption_period_end": "0001-01-01T00:00:00Z", + "owner_id": "org-royal-sun-91776391", + "compute_last_active_at": "2025-09-10T07:58:21Z", + "org_id": "org-royal-sun-91776391" + } +} +``` + +### Retrieve connection URI + +1. Action: Retrieves a ready-to-use connection URI for a specific database within a project. +2. Endpoint: `GET /projects/{project_id}/connection_uri` +3. Prerequisites: You must know the `project_id`, `database_name`, and `role_name`. +4. Query Parameters: + - `project_id` (path, required): The unique identifier of the project. + - `database_name` (query, required): The name of the target database. + - `role_name` (query, required): The role to use for the connection. + - `branch_id` (query, optional): The branch ID. Defaults to the project's primary branch if not specified. + - `pooled` (query, optional, boolean): If set to `false`, returns a direct connection URI instead of a pooled one. Defaults to `true`. + - `endpoint_id` (query, optional): The specific endpoint ID to connect to. Defaults to the `read-write` endpoint_id associated with the `branch_id` if not specified. + +Example request: + +```bash +curl 'https://console.neon.tech/api/v2/projects/old-fire-32990194/connection_uri?database_name=neondb&role_name=neondb_owner' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" +``` + +Example response: + +```json +{ + "uri": "postgresql://neondb_owner:npg_IDNnorOST71P@ep-shiny-morning-a1bfdvjs-pooler.ap-southeast-1.aws.neon.tech/neondb?channel_binding=require&sslmode=require" +} +``` diff --git a/.agents/skills/neon-postgres/references/neon-serverless.md b/.agents/skills/neon-postgres/references/neon-serverless.md new file mode 100644 index 00000000..79400cb7 --- /dev/null +++ b/.agents/skills/neon-postgres/references/neon-serverless.md @@ -0,0 +1,254 @@ +# Neon Serverless Driver + +Patterns and best practices for connecting to Neon databases in serverless environments using the `@neondatabase/serverless` driver. The driver connects over **HTTP** for fast, single queries or **WebSockets** for `node-postgres` compatibility and interactive transactions. + +For official documentation: + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/serverless/serverless-driver +``` + +## Installation + +```bash +# Using npm +npm install @neondatabase/serverless + +# Using JSR +bunx jsr add @neon/serverless +``` + +**Note:** Version 1.0.0+ requires **Node.js v19 or later**. + +For projects that depend on `pg` but want to use Neon's WebSocket-based connection pool: + +```json +"dependencies": { + "pg": "npm:@neondatabase/serverless@^0.10.4" +}, +"overrides": { + "pg": "npm:@neondatabase/serverless@^0.10.4" +} +``` + +## Connection String + +Always use environment variables: + +```typescript +// For HTTP queries +import { neon } from "@neondatabase/serverless"; +const sql = neon(process.env.DATABASE_URL!); + +// For WebSocket connections +import { Pool } from "@neondatabase/serverless"; +const pool = new Pool({ connectionString: process.env.DATABASE_URL! }); +``` + +**Never hardcode credentials:** + +```typescript +// AVOID +const sql = neon("postgres://username:password@host.neon.tech/neondb"); +``` + +## HTTP Queries with `neon` function + +Ideal for simple, "one-shot" queries in serverless/edge environments. Uses HTTP `fetch` - fastest method for single queries. + +### Parameterized Queries + +Use tagged template literals for safe parameter interpolation: + +```typescript +const [post] = await sql`SELECT * FROM posts WHERE id = ${postId}`; +``` + +For manually constructed queries: + +```typescript +const [post] = await sql.query("SELECT * FROM posts WHERE id = $1", [postId]); +``` + +**Never concatenate user input:** + +```typescript +// AVOID: SQL Injection Risk +const [post] = await sql("SELECT * FROM posts WHERE id = " + postId); +``` + +### Configuration Options + +```typescript +// Return rows as arrays instead of objects +const sqlArrayMode = neon(process.env.DATABASE_URL!, { arrayMode: true }); +const rows = await sqlArrayMode`SELECT id, title FROM posts`; +// rows -> [[1, "First Post"], [2, "Second Post"]] + +// Get full results including row count and field metadata +const sqlFull = neon(process.env.DATABASE_URL!, { fullResults: true }); +const result = await sqlFull`SELECT * FROM posts LIMIT 1`; +// result -> { rows: [...], fields: [...], rowCount: 1, ... } +``` + +## WebSocket Connections with `Pool` and `Client` + +Use for `node-postgres` compatibility, interactive transactions, or session support. + +### WebSocket Configuration + +For Node.js v21 and earlier: + +```typescript +import { Pool, neonConfig } from "@neondatabase/serverless"; +import ws from "ws"; + +// Required for Node.js < v22 +neonConfig.webSocketConstructor = ws; + +const pool = new Pool({ connectionString: process.env.DATABASE_URL! }); +``` + +### Serverless Lifecycle Management + +Create, use, and close the pool within the same invocation: + +```typescript +// Vercel Edge Functions example +export default async (req: Request, ctx: ExecutionContext) => { + const pool = new Pool({ connectionString: process.env.DATABASE_URL! }); + + try { + const { rows } = await pool.query("SELECT * FROM users"); + return new Response(JSON.stringify(rows)); + } catch (err) { + console.error(err); + return new Response("Database error", { status: 500 }); + } finally { + ctx.waitUntil(pool.end()); + } +}; +``` + +**Avoid** creating a global `Pool` instance outside the handler. + +## Transactions + +### HTTP Transactions + +For running multiple queries in a single, non-interactive transaction: + +```typescript +const [newUser, newProfile] = await sql.transaction( + [ + sql`INSERT INTO users(name) VALUES(${name}) RETURNING id`, + sql`INSERT INTO profiles(user_id, bio) VALUES(${userId}, ${bio})`, + ], + { + isolationLevel: "ReadCommitted", + readOnly: false, + }, +); +``` + +### Interactive Transactions + +For complex transactions with conditional logic: + +```typescript +const pool = new Pool({ connectionString: process.env.DATABASE_URL! }); +const client = await pool.connect(); +try { + await client.query("BEGIN"); + const { + rows: [{ id }], + } = await client.query("INSERT INTO users(name) VALUES($1) RETURNING id", [ + name, + ]); + await client.query("INSERT INTO profiles(user_id, bio) VALUES($1, $2)", [ + id, + bio, + ]); + await client.query("COMMIT"); +} catch (err) { + await client.query("ROLLBACK"); + throw err; +} finally { + client.release(); + await pool.end(); +} +``` + +## Environment-Specific Optimizations + +```javascript +// For Vercel Edge Functions, specify nearest region +export const config = { + runtime: "edge", + regions: ["iad1"], // Region nearest to your Neon DB +}; + +// For Cloudflare Workers, consider using Hyperdrive +// https://neon.com/blog/hyperdrive-neon-faq +``` + +## ORM Integration + +For Drizzle ORM integration with the serverless driver, see `neon-drizzle.md`. + +### Prisma + +```typescript +import { neonConfig } from "@neondatabase/serverless"; +import { PrismaNeon, PrismaNeonHTTP } from "@prisma/adapter-neon"; +import { PrismaClient } from "@prisma/client"; +import ws from "ws"; + +const connectionString = process.env.DATABASE_URL; +neonConfig.webSocketConstructor = ws; + +// HTTP adapter +const adapterHttp = new PrismaNeonHTTP(connectionString!, {}); +export const prismaClientHttp = new PrismaClient({ adapter: adapterHttp }); + +// WebSocket adapter +const adapterWs = new PrismaNeon({ connectionString }); +export const prismaClientWs = new PrismaClient({ adapter: adapterWs }); +``` + +### Kysely + +```typescript +import { Pool } from "@neondatabase/serverless"; +import { Kysely, PostgresDialect } from "kysely"; + +const dialect = new PostgresDialect({ + pool: new Pool({ connectionString: process.env.DATABASE_URL }), +}); + +const db = new Kysely({ dialect }); +``` + +**NOTE:** Do not pass the `neon()` function to ORMs that expect a `node-postgres` compatible `Pool`. + +## Error Handling + +```javascript +// Pool error handling +const pool = new Pool({ connectionString: process.env.DATABASE_URL }); +pool.on("error", (err) => { + console.error("Unexpected error on idle client", err); + process.exit(-1); +}); + +// Query error handling +try { + const [post] = await sql`SELECT * FROM posts WHERE id = ${postId}`; + if (!post) { + return new Response("Not found", { status: 404 }); + } +} catch (err) { + console.error("Database query failed:", err); + return new Response("Server error", { status: 500 }); +} +``` diff --git a/.agents/skills/neon-postgres/references/neon-typescript-sdk.md b/.agents/skills/neon-postgres/references/neon-typescript-sdk.md new file mode 100644 index 00000000..51461bf1 --- /dev/null +++ b/.agents/skills/neon-postgres/references/neon-typescript-sdk.md @@ -0,0 +1,334 @@ +# Neon TypeScript SDK + +The `@neondatabase/api-client` TypeScript SDK is a typed wrapper around the Neon REST API. It provides methods for managing all Neon resources, including projects, branches, endpoints, roles, and databases. + +For core concepts (Organization, Project, Branch, Endpoint, etc.), see `what-is-neon.md`. + +## Documentation + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/reference/typescript-sdk +``` + +## Installation + +```bash +npm install @neondatabase/api-client +``` + +## Authentication + +```typescript +import { createApiClient } from "@neondatabase/api-client"; + +const apiKey = process.env.NEON_API_KEY; +if (!apiKey) { + throw new Error("NEON_API_KEY environment variable is not set."); +} + +const apiClient = createApiClient({ apiKey }); +``` + +## Projects + +### List Projects + +```typescript +const response = await apiClient.listProjects({}); +console.log("Projects:", response.data.projects); +``` + +### Create Project + +```typescript +const response = await apiClient.createProject({ + project: { name: "my-project", pg_version: 17, region_id: "aws-us-east-2" }, +}); +console.log( + "Connection URI:", + response.data.connection_uris[0]?.connection_uri, +); +``` + +### Get Project + +```typescript +const response = await apiClient.getProject("your-project-id"); +``` + +### Update Project + +```typescript +await apiClient.updateProject("your-project-id", { + project: { name: "new-name" }, +}); +``` + +### Delete Project + +```typescript +await apiClient.deleteProject("project-id"); +``` + +### Get Connection URI + +```typescript +const response = await apiClient.getConnectionUri({ + projectId: "your-project-id", + database_name: "neondb", + role_name: "neondb_owner", + pooled: true, +}); +console.log("URI:", response.data.uri); +``` + +## Branches + +### Create Branch + +```typescript +import { EndpointType } from "@neondatabase/api-client"; + +const response = await apiClient.createProjectBranch("your-project-id", { + branch: { name: "feature-branch" }, + endpoints: [{ type: EndpointType.ReadWrite, autoscaling_limit_max_cu: 1 }], +}); +``` + +### List Branches + +```typescript +const response = await apiClient.listProjectBranches({ + projectId: "your-project-id", +}); +``` + +### Get Branch + +```typescript +const response = await apiClient.getProjectBranch("your-project-id", "br-xxx"); +``` + +### Update Branch + +```typescript +await apiClient.updateProjectBranch("your-project-id", "br-xxx", { + branch: { name: "updated-name" }, +}); +``` + +### Delete Branch + +```typescript +await apiClient.deleteProjectBranch("your-project-id", "br-xxx"); +``` + +## Databases + +### Create Database + +```typescript +await apiClient.createProjectBranchDatabase("your-project-id", "br-xxx", { + database: { name: "my-app-db", owner_name: "neondb_owner" }, +}); +``` + +### List Databases + +```typescript +const response = await apiClient.listProjectBranchDatabases( + "your-project-id", + "br-xxx", +); +``` + +### Delete Database + +```typescript +await apiClient.deleteProjectBranchDatabase( + "your-project-id", + "br-xxx", + "my-app-db", +); +``` + +## Roles + +### Create Role + +```typescript +const response = await apiClient.createProjectBranchRole( + "your-project-id", + "br-xxx", + { + role: { name: "app_user" }, + }, +); +console.log("Password:", response.data.role.password); +``` + +### List Roles + +```typescript +const response = await apiClient.listProjectBranchRoles( + "your-project-id", + "br-xxx", +); +``` + +### Delete Role + +```typescript +await apiClient.deleteProjectBranchRole( + "your-project-id", + "br-xxx", + "app_user", +); +``` + +## Endpoints + +### Create Endpoint + +```typescript +import { EndpointType } from "@neondatabase/api-client"; + +const response = await apiClient.createProjectEndpoint("your-project-id", { + endpoint: { branch_id: "br-xxx", type: EndpointType.ReadOnly }, +}); +``` + +### List Endpoints + +```typescript +const response = await apiClient.listProjectEndpoints("your-project-id"); +``` + +### Start/Suspend/Restart Endpoint + +```typescript +// Start +await apiClient.startProjectEndpoint("your-project-id", "ep-xxx"); + +// Suspend +await apiClient.suspendProjectEndpoint("your-project-id", "ep-xxx"); + +// Restart +await apiClient.restartProjectEndpoint("your-project-id", "ep-xxx"); +``` + +### Update Endpoint + +```typescript +await apiClient.updateProjectEndpoint("your-project-id", "ep-xxx", { + endpoint: { autoscaling_limit_max_cu: 2 }, +}); +``` + +### Delete Endpoint + +```typescript +await apiClient.deleteProjectEndpoint("your-project-id", "ep-xxx"); +``` + +## API Keys + +### List API Keys + +```typescript +const response = await apiClient.listApiKeys(); +``` + +### Create API Key + +```typescript +const response = await apiClient.createApiKey({ key_name: "my-script-key" }); +console.log("Key:", response.data.key); // Store securely! +``` + +### Revoke API Key + +```typescript +await apiClient.revokeApiKey(1234); +``` + +## Operations + +### List Operations + +```typescript +const response = await apiClient.listProjectOperations({ + projectId: "your-project-id", +}); +``` + +### Get Operation + +```typescript +const response = await apiClient.getProjectOperation( + "your-project-id", + "op-xxx", +); +``` + +## Organizations + +### Get Organization + +```typescript +const response = await apiClient.getOrganization("org-xxx"); +``` + +### List Members + +```typescript +const response = await apiClient.getOrganizationMembers("org-xxx"); +``` + +### Create Org API Key + +```typescript +const response = await apiClient.createOrgApiKey("org-xxx", { + key_name: "ci-key", + project_id: "project-xxx", // Optional: scope to project +}); +``` + +### Invite Member + +```typescript +import { MemberRole } from "@neondatabase/api-client"; + +await apiClient.createOrganizationInvitations("org-xxx", { + invitations: [{ email: "dev@example.com", role: MemberRole.Member }], +}); +``` + +## Error Handling + +```typescript +async function safeApiOperation(projectId: string) { + try { + const response = await apiClient.getProject(projectId); + return response.data; + } catch (error: any) { + if (error.isAxiosError) { + const status = error.response?.status; + switch (status) { + case 401: + console.error("Check your NEON_API_KEY"); + break; + case 404: + console.error("Resource not found"); + break; + case 429: + console.error("Rate limit exceeded"); + break; + default: + console.error("API error:", error.response?.data?.message); + } + } + return null; + } +} +``` diff --git a/.agents/skills/neon-postgres/references/referencing-docs.md b/.agents/skills/neon-postgres/references/referencing-docs.md new file mode 100644 index 00000000..a3d98d2d --- /dev/null +++ b/.agents/skills/neon-postgres/references/referencing-docs.md @@ -0,0 +1,78 @@ +# Referencing Neon Docs + +The Neon documentation is the source of truth for all Neon-related information. Always verify Neon-related claims, configurations, and best practices against the official documentation. + +## Getting the Documentation Index + +To get a list of all available Neon documentation pages: + +```bash +curl https://neon.com/llms.txt +``` + +This returns an index of all documentation pages with their URLs and descriptions. + +## Fetching Individual Documentation Pages + +To fetch any documentation page as markdown for review: + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/ +``` + +**Examples:** + +```bash +# Fetch the API reference +curl -H "Accept: text/markdown" https://neon.com/docs/reference/api-reference + +# Fetch connection pooling docs +curl -H "Accept: text/markdown" https://neon.com/docs/connect/connection-pooling + +# Fetch branching documentation +curl -H "Accept: text/markdown" https://neon.com/docs/introduction/branching + +# Fetch serverless driver docs +curl -H "Accept: text/markdown" https://neon.com/docs/serverless/serverless-driver +``` + +## Common Documentation Paths + +| Topic | Path | +| ------------------ | ------------------------------------ | +| Introduction | `/docs/introduction` | +| Branching | `/docs/introduction/branching` | +| Autoscaling | `/docs/introduction/autoscaling` | +| Scale to Zero | `/docs/introduction/scale-to-zero` | +| Connection Pooling | `/docs/connect/connection-pooling` | +| Serverless Driver | `/docs/serverless/serverless-driver` | +| JavaScript SDK | `/docs/reference/javascript-sdk` | +| API Reference | `/docs/reference/api-reference` | +| TypeScript SDK | `/docs/reference/typescript-sdk` | +| Python SDK | `/docs/reference/python-sdk` | + +## Framework and Language Guides + +```bash +# Next.js +curl -H "Accept: text/markdown" https://neon.com/docs/guides/nextjs + +# Django +curl -H "Accept: text/markdown" https://neon.com/docs/guides/django + +# Drizzle ORM +curl -H "Accept: text/markdown" https://neon.com/docs/guides/drizzle + +# Prisma +curl -H "Accept: text/markdown" https://neon.com/docs/guides/prisma +``` + +## Best Practices + +1. **Always verify** - When answering questions about Neon features, APIs, or configurations, fetch the relevant documentation to verify your response is accurate. + +2. **Check llms.txt first** - If you're unsure which documentation page covers a topic, fetch the llms.txt index to find the relevant URL. Don't make up URLs. + +3. **Docs are the source of truth** - If there's any conflict between your training data and the documentation, the documentation is correct. Neon features and APIs evolve, so always defer to the current docs. + +4. **Cite your sources** - When providing information from the docs, reference the documentation URL so users can read more if needed. diff --git a/.agents/skills/neon-postgres/references/what-is-neon.md b/.agents/skills/neon-postgres/references/what-is-neon.md new file mode 100644 index 00000000..6b8c759d --- /dev/null +++ b/.agents/skills/neon-postgres/references/what-is-neon.md @@ -0,0 +1,57 @@ +# What is Neon + +Neon is a serverless Postgres platform designed to help you build reliable and scalable applications faster. It separates compute and storage to offer modern developer features such as autoscaling, branching, instant restore, and scale-to-zero. + +For the full introduction, fetch the official docs: + +```bash +curl -H "Accept: text/markdown" https://neon.com/docs/introduction +``` + +## Core Concepts + +Understanding Neon's resource hierarchy is essential for working with the platform effectively. + +| Concept | Description | Key Relationship | +| ---------------- | --------------------------------------------------------------------- | ------------------------- | +| Organization | Highest-level container for billing, users, and projects | Contains Projects | +| Project | Primary container for all database resources for an application | Contains Branches | +| Branch | Lightweight, copy-on-write clone of database state | Contains Databases, Roles | +| Compute Endpoint | Running PostgreSQL instance (CPU/RAM for queries) | Attached to a Branch | +| Database | Logical container for data (tables, schemas, views) | Exists within a Branch | +| Role | PostgreSQL role for authentication and authorization | Belongs to a Branch | +| Operation | Async action by the control plane (creating branch, starting compute) | Associated with Project | + +## Key Differentiators + +1. **Serverless Architecture**: Compute scales automatically and can suspend when idle +2. **Branching**: Create instant database copies without duplicating storage +3. **Separation of Compute and Storage**: Pay for compute only when active +4. **Postgres Compatible**: Works with any Postgres driver, ORM, or tool + +## Documentation Resources + +| Topic | Documentation URL | +| ---------------------- | --------------------------------------------------------- | +| Introduction | https://neon.com/docs/introduction | +| Architecture | https://neon.com/docs/introduction/architecture-overview | +| Plans & Billing | https://neon.com/docs/introduction/about-billing | +| Regions | https://neon.com/docs/introduction/regions | +| Postgres Compatibility | https://neon.com/docs/reference/compatibility | + +```bash +# Fetch architecture docs +curl -H "Accept: text/markdown" https://neon.com/docs/introduction/architecture-overview + +# Fetch plans and billing +curl -H "Accept: text/markdown" https://neon.com/docs/introduction/about-billing +``` + +## When to Use Neon + +Neon is ideal for: + +- **Serverless applications**: Functions that need database access without managing connections +- **Development workflows**: Branch databases like code for isolated testing +- **Variable workloads**: Auto-scale during traffic spikes, scale to zero when idle +- **Cost optimization**: Pay only for active compute time and storage used diff --git a/.cursor/skills/agent-browser b/.cursor/skills/agent-browser new file mode 120000 index 00000000..e298b7be --- /dev/null +++ b/.cursor/skills/agent-browser @@ -0,0 +1 @@ +../../.agents/skills/agent-browser \ No newline at end of file diff --git a/.cursor/skills/neon-postgres b/.cursor/skills/neon-postgres new file mode 120000 index 00000000..54bdeeff --- /dev/null +++ b/.cursor/skills/neon-postgres @@ -0,0 +1 @@ +../../.agents/skills/neon-postgres \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 2f7fcc64..c0d7ad1c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,6 +7,7 @@ This file provides guidance to AI agents when working with code in this reposito This is the **Neon MCP Server** - a Model Context Protocol server that bridges natural language requests to the Neon API, enabling LLMs to manage Neon Postgres databases through conversational commands. The project implements both local (stdio) and remote (SSE/Streamable HTTP) MCP server transports with OAuth authentication support. **Architecture Note**: The entire project is a unified Next.js application in the `landing/` directory that serves dual purposes: + 1. **Remote MCP Server**: Deployed on Vercel serverless infrastructure, accessible at `mcp.neon.tech` 2. **Local MCP CLI**: Published as `@neondatabase/mcp-server-neon` npm package, runs locally via stdio transport @@ -29,6 +30,9 @@ bun run build:cli # Run the CLI locally with API key bun run start:cli $NEON_API_KEY +# Run the CLI with access control flags +bun run start:cli $NEON_API_KEY -- --preset local_development --project-id my-project --protect-production + # Or run the built CLI directly node dist/cli/cli.js start ``` @@ -57,7 +61,6 @@ bun run typecheck ### Core Components 1. **MCP Server (`landing/mcp-src/server/index.ts`)** - - Creates and configures the MCP server instance - Registers all tools and resources from centralized definitions - Implements error handling and observability (Sentry, analytics) @@ -69,24 +72,20 @@ bun run typecheck - Falls back gracefully when project-scoped keys cannot access account-level endpoints 2. **Tools System (`landing/mcp-src/tools/`)** - - `definitions.ts`: Exports `NEON_TOOLS` array defining all available tools with their schemas - `tools.ts`: Exports `NEON_HANDLERS` object mapping tool names to handler functions - `toolsSchema.ts`: Zod schemas for tool input validation - `handlers/`: Individual tool handler implementations organized by feature 3. **CLI Entry Point (`landing/mcp-src/cli.ts`)** - - Entry point for the npm package CLI - Handles stdio transport for local MCP clients (Claude Desktop, Cursor) 4. **Remote Transport (`landing/app/api/[transport]/route.ts`)** - - Next.js API route handling SSE and Streamable HTTP transports - Uses `mcp-handler` library for serverless MCP protocol handling 5. **OAuth System (`landing/lib/oauth/` and `landing/mcp-src/oauth/`)** - - OAuth 2.0 server implementation for remote MCP authentication - Integrates with Neon's OAuth provider (UPSTREAM_OAUTH_HOST) - Token persistence using Keyv with Postgres backend @@ -104,9 +103,9 @@ bun run typecheck - **Stateless Design**: The server is designed for serverless deployment. Tools like migrations and query tuning create temporary branches but do NOT store state in memory. Instead, all context (branch IDs, migration SQL, etc.) is returned to the LLM, which passes it back to subsequent tool calls. This enables horizontal scaling on Vercel. -- **Read-Only Mode** (`landing/mcp-src/utils/read-only.ts`): Tools define a `readOnlySafe` property. When the server runs in read-only mode, only tools marked as `readOnlySafe: true` are available. Read-only mode is determined by priority: `X-READ-ONLY` header > OAuth scope (only `read` scope = read-only) > default (false). The module also exports `SCOPE_DEFINITIONS` for human-readable scope labels and `hasWriteScope()` to check for write permissions. +- **Read-Only Mode** (`landing/mcp-src/utils/read-only.ts`): Tools define a `readOnlySafe` property. When the server runs in read-only mode, only tools marked as `readOnlySafe: true` are available. The module also exports `SCOPE_DEFINITIONS` for human-readable scope labels and `hasWriteScope()` to check for write permissions. -- **OAuth Scope Selection UI**: During OAuth authorization, users see a permissions dialog where they can select which scopes to grant. Read access is always granted, while write access can be opted out of. The authorization endpoint (`landing/app/api/authorize/route.ts`) renders this UI and processes scope selections. +- **OAuth Authorization UI**: During OAuth authorization, users see a permissions dialog rendered by `landing/app/api/authorize/route.ts`. The UI presents **permission presets** as radio buttons (`full_access`, `local_development`, `production_use`, `custom`) and a **"Protect production branches"** checkbox. The selected preset and protection settings are encoded into the grant context and forwarded through the OAuth flow. When a client has already been approved (tracked via signed cookies), the dialog is skipped. - **MCP Tool Annotations**: All tools include MCP-standard annotations for client hints: - `title`: Human-readable tool name @@ -115,6 +114,13 @@ bun run typecheck - `idempotentHint`: Whether repeated calls produce the same result - `openWorldHint`: Whether the tool interacts with external systems +- **Fine-Grained Access Control** (`landing/mcp-src/utils/grant-context.ts`, `landing/mcp-src/tools/grant-filter.ts`, `landing/mcp-src/tools/grant-enforcement.ts`): The server supports access control through permission presets, project scoping, production branch protection, and custom scope categories. See `README.md` for full user-facing documentation of all supported headers, flags, and values. The **source of truth for access control behavior** (exact tool counts, filtering pipeline, edge cases, and precedence rules) is the test suite in `landing/mcp-src/__tests__/`: + - `grant-filter.test.ts`: Tool inventory integrity, preset/scope/project filtering, exact tool count matrix, and access control warnings + - `list-tools-endpoint.test.ts`: End-to-end header-based filtering via the `/api/list-tools` endpoint + - `read-only.test.ts`: Read-only priority chain (headers > preset > OAuth scope > default) + - `grant-context.test.ts`: Header/CLI/token grant parsing, scope category validation, precedence rules + - `grant-enforcement.test.ts`: Runtime branch protection enforcement + - **Analytics & Observability**: Every tool call, resource access, and error is tracked through Segment analytics and Sentry error reporting. ## Adding New Tools @@ -123,7 +129,7 @@ bun run typecheck ```typescript export const myNewToolInputSchema = z.object({ - project_id: z.string().describe('The Neon project ID'), + project_id: z.string().describe("The Neon project ID"), // ... other fields }); ``` @@ -149,10 +155,10 @@ export const myNewToolInputSchema = z.object({ 3. Create a handler in `landing/mcp-src/tools/handlers/my-new-tool.ts`: ```typescript -import { ToolHandler } from '../types'; -import { myNewToolInputSchema } from '../toolsSchema'; +import { ToolHandler } from "../types"; +import { myNewToolInputSchema } from "../toolsSchema"; -export const myNewToolHandler: ToolHandler<'my_new_tool'> = async ( +export const myNewToolHandler: ToolHandler<"my_new_tool"> = async ( args, neonClient, extra, @@ -161,8 +167,8 @@ export const myNewToolHandler: ToolHandler<'my_new_tool'> = async ( return { content: [ { - type: 'text', - text: 'Result message', + type: "text", + text: "Result message", }, ], }; @@ -172,7 +178,7 @@ export const myNewToolHandler: ToolHandler<'my_new_tool'> = async ( 4. Register the handler in `landing/mcp-src/tools/tools.ts`: ```typescript -import { myNewToolHandler } from './handlers/my-new-tool'; +import { myNewToolHandler } from "./handlers/my-new-tool"; export const NEON_HANDLERS = { // ... existing handlers @@ -198,30 +204,35 @@ landing/ # Next.js app (main project) ├── app/ # Next.js App Router │ ├── api/ # API routes for remote MCP server │ │ ├── [transport]/route.ts # Main MCP handler (SSE/Streamable HTTP) -│ │ ├── authorize/ # OAuth authorization endpoint +│ │ ├── authorize/ # OAuth authorization endpoint (renders preset UI) │ │ ├── token/ # OAuth token exchange │ │ ├── register/ # Dynamic client registration │ │ ├── revoke/ # OAuth token revocation │ │ └── health/ # Health check endpoint │ ├── callback/ # OAuth callback handler │ └── .well-known/ # OAuth discovery endpoints +│ # Note: Root `/` redirects to https://neon.tech/docs/ai/neon-mcp-server +│ # (configured in next.config.ts). There is no landing page. ├── lib/ # Next.js-compatible utilities │ ├── config.ts # Centralized configuration │ └── oauth/ # OAuth utilities for Next.js ├── mcp-src/ # MCP server source code │ ├── cli.ts # CLI entry point (stdio transport) +│ ├── initConfig.ts # CLI argument parsing (--preset, --project-id, etc.) │ ├── server/ # MCP server factory │ │ ├── index.ts # Server creation and tool registration │ │ ├── api.ts # Neon API client factory │ │ ├── account.ts # Account resolution (user/org/project-scoped) │ │ └── errors.ts # Error handling utilities │ ├── tools/ # Tool definitions and handlers -│ │ ├── definitions.ts # Tool definitions (NEON_TOOLS) with annotations -│ │ ├── tools.ts # Tool handlers mapping (NEON_HANDLERS) -│ │ ├── toolsSchema.ts # Zod schemas for tool inputs -│ │ ├── handlers/ # Individual tool implementations -│ │ ├── types.ts # TypeScript types -│ │ └── utils.ts # Tool utilities +│ │ ├── definitions.ts # Tool definitions (NEON_TOOLS) with annotations +│ │ ├── tools.ts # Tool handlers mapping (NEON_HANDLERS) +│ │ ├── toolsSchema.ts # Zod schemas for tool inputs +│ │ ├── handlers/ # Individual tool implementations +│ │ ├── grant-filter.ts # Filters tools based on grant context (presets/scopes) +│ │ ├── grant-enforcement.ts # Enforces branch protection at runtime +│ │ ├── types.ts # TypeScript types +│ │ └── utils.ts # Tool utilities │ ├── oauth/ # OAuth model and KV store │ ├── analytics/ # Segment analytics │ ├── sentry/ # Sentry error tracking @@ -229,18 +240,21 @@ landing/ # Next.js app (main project) │ │ └── stdio.ts # Stdio transport for CLI │ ├── types/ # Shared TypeScript types │ ├── utils/ # Shared utilities -│ │ ├── read-only.ts # Read-only mode detection, scope definitions -│ │ ├── trace.ts # TraceId generation for request correlation +│ │ ├── read-only.ts # Read-only mode detection, scope definitions +│ │ ├── grant-context.ts # Grant context types, preset definitions, CLI grant parsing +│ │ ├── trace.ts # TraceId generation for request correlation │ │ ├── client-application.ts # Client application utilities -│ │ ├── logger.ts # Logging utilities -│ │ └── polyfills.ts # Runtime polyfills +│ │ ├── logger.ts # Logging utilities +│ │ └── polyfills.ts # Runtime polyfills │ ├── resources.ts # MCP resources │ ├── prompts.ts # LLM prompts │ └── constants.ts # Shared constants -├── components/ # React components for landing page +├── components/ # React components (OAuth UI, shared UI primitives) +├── patches/ # npm package patches ├── public/ # Static assets ├── package.json # Package configuration ├── tsconfig.json # TypeScript config (bundler resolution) +├── next.config.ts # Next.js config (redirects, rewrites) ├── vercel.json # Vercel deployment config └── vercel-migration.md # Migration documentation @@ -276,40 +290,40 @@ The remote MCP server (`mcp.neon.tech`) is deployed on Vercel's serverless infra ### API Endpoints -| Route | Purpose | -|-------|---------| -| `/api/mcp` | Streamable HTTP transport (recommended) | -| `/api/sse` | Server-Sent Events transport (deprecated) | -| `/api/authorize` | OAuth authorization initiation | -| `/callback` | OAuth callback handler | -| `/api/token` | OAuth token exchange | -| `/api/revoke` | OAuth token revocation | -| `/api/register` | Dynamic client registration | +| Route | Purpose | +| ----------------------------------------- | --------------------------------------------------- | +| `/api/mcp` | Streamable HTTP transport (recommended) | +| `/api/sse` | Server-Sent Events transport (deprecated) | +| `/api/authorize` | OAuth authorization initiation | +| `/callback` | OAuth callback handler | +| `/api/token` | OAuth token exchange | +| `/api/revoke` | OAuth token revocation | +| `/api/register` | Dynamic client registration | | `/.well-known/oauth-authorization-server` | OAuth server metadata (includes `scopes_supported`) | -| `/.well-known/oauth-protected-resource` | OAuth protected resource metadata | +| `/.well-known/oauth-protected-resource` | OAuth protected resource metadata | -### OAuth Scopes +### OAuth Scopes and Presets -The server supports three scopes: `read`, `write`, and `*`. These are exposed via the `/.well-known/oauth-authorization-server` endpoint's `scopes_supported` field. +The server supports three OAuth scopes: `read`, `write`, and `*`. These are exposed via the `/.well-known/oauth-authorization-server` endpoint's `scopes_supported` field. - **`read`**: Read-only access to Neon resources - **`write`**: Full access including create/delete operations - **`*`**: Wildcard, equivalent to full access -During authorization, users can uncheck "Full access" to request only `read` scope, which enables read-only mode. +During OAuth authorization, the UI presents **permission presets** (radio buttons: Full Access, Local Development, Production Use) and a **"Protect production branches"** checkbox. The selected preset and grant context are encoded into the OAuth state and forwarded through the flow. ### Environment Variables (Vercel) -| Variable | Description | -|----------|-------------| -| `SERVER_HOST` | Server URL (falls back to `VERCEL_URL`) | -| `UPSTREAM_OAUTH_HOST` | Neon OAuth provider URL | -| `CLIENT_ID` / `CLIENT_SECRET` | OAuth client credentials | -| `COOKIE_SECRET` | Secret for signed cookies | -| `KV_URL` | Vercel KV (Upstash Redis) URL | -| `OAUTH_DATABASE_URL` | Postgres URL for token storage | -| `SENTRY_DSN` | Sentry error tracking DSN | -| `ANALYTICS_WRITE_KEY` | Segment analytics write key | +| Variable | Description | +| ----------------------------- | --------------------------------------- | +| `SERVER_HOST` | Server URL (falls back to `VERCEL_URL`) | +| `UPSTREAM_OAUTH_HOST` | Neon OAuth provider URL | +| `CLIENT_ID` / `CLIENT_SECRET` | OAuth client credentials | +| `COOKIE_SECRET` | Secret for signed cookies | +| `KV_URL` | Vercel KV (Upstash Redis) URL | +| `OAUTH_DATABASE_URL` | Postgres URL for token storage | +| `SENTRY_DSN` | Sentry error tracking DSN | +| `ANALYTICS_WRITE_KEY` | Segment analytics write key | ### Development Notes @@ -323,6 +337,7 @@ During authorization, users can uncheck "Full access" to request only `read` sco The `deploy-preview.yml` workflow enables deploying PRs to the preview environment (`preview-mcp.neon.tech`) for testing OAuth flows and remote MCP functionality. **Usage:** + 1. Add the `deploy-preview` label to a PR 2. The workflow pushes to the `preview` branch, which triggers Vercel deployment 3. Only one PR can own the preview environment at a time (label is auto-removed from other PRs) @@ -335,11 +350,13 @@ The `deploy-preview.yml` workflow enables deploying PRs to the preview environme The `claude.yml` workflow enables interactive Claude assistance in issues and pull requests. **Usage:** + - Mention `@claude` in any issue, PR comment, or PR review comment - Claude will analyze and respond to your request - Only works for OWNER/MEMBER/COLLABORATOR to prevent abuse **Available Commands:** + - GitHub CLI commands (`gh issue:*`, `gh pr:*`, `gh search:*`) - Can help with code review, issue triage, and PR descriptions @@ -381,3 +398,84 @@ This repository uses an enhanced Claude Code Review workflow that provides inlin - **Automatic**: Opens when PR is created - **Manual**: Run workflow via GitHub Actions with PR number - **Security**: Only OWNER/MEMBER/COLLABORATOR PRs (blocks external) + +## Testing the OAuth Consent Page Locally + +The OAuth authorization page (`/api/authorize`) requires a registered OAuth client in the database. Since the dev server uses `OAUTH_DATABASE_URL` from `.env.local` for client storage, you need to register a client first. + +### 1. Register an OAuth Client + +With the dev server running (`cd landing && bun run dev`), use `curl` to dynamically register a client: + +```bash +curl -s -X POST http://localhost:3000/api/register \ + -H "Content-Type: application/json" \ + -d '{ + "client_name": "My Test Client", + "redirect_uris": ["http://localhost:3000/callback"], + "grant_types": ["authorization_code"], + "response_types": ["code"], + "token_endpoint_auth_method": "none" + }' +``` + +This returns a JSON response with `client_id` and `client_secret`. Save the `client_id` — you'll need it for the authorize URL. + +**Important**: The `grant_types`, `response_types`, and `token_endpoint_auth_method` fields are required. Omitting them will return a 400 error. + +### 2. Visit the OAuth Consent Page + +Build the authorize URL with the registered `client_id`: + +``` +http://localhost:3000/api/authorize?response_type=code&client_id=&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&state=test&code_challenge=test123456789012345678901234567890123456789&code_challenge_method=S256 +``` + +Open this URL in a browser to see the OAuth consent page with preset tabs, scope categories, and branch protection options. + +**Note**: After approving once, a signed cookie remembers the approval and will skip the dialog on subsequent visits. Use an incognito window or clear cookies to see the consent page again. + +### 3. Take Screenshots with agent-browser + +Use `agent-browser` to automate screenshots of different OAuth consent page states: + +```bash +# Open the OAuth consent page +agent-browser open "http://localhost:3000/api/authorize?response_type=code&client_id=&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&state=test&code_challenge=test123456789012345678901234567890123456789&code_challenge_method=S256" + +# Take a full-page screenshot of the default view (Full Access preset) +agent-browser screenshot --full screenshots/full-access.png + +# Get interactive elements to find button refs +agent-browser snapshot -i +# Output: button "Custom" [ref=e2], button "Local Development" [ref=e3], etc. + +# Click different presets and screenshot each +agent-browser click @e2 # Custom preset +agent-browser screenshot --full screenshots/custom-preset.png + +agent-browser click @e3 # Local Development preset +agent-browser screenshot --full screenshots/local-dev.png + +# Expand permission details (collapsible section on non-custom presets) +agent-browser find text "Show permission details" click +agent-browser screenshot --full screenshots/local-dev-expanded.png + +# Check the branch protection checkbox +agent-browser check @e6 +agent-browser screenshot --full screenshots/with-branch-protection.png + +# Clean up +agent-browser close +``` + +**agent-browser quick reference**: + +- `agent-browser open ` — Navigate to page +- `agent-browser snapshot -i` — Get interactive elements with refs (`@e1`, `@e2`) +- `agent-browser click @e1` / `fill @e2 "text"` — Interact using refs +- `agent-browser screenshot [path.png]` — Screenshot (add `--full` for full page) +- `agent-browser find text "..." click` — Find by text and click +- `agent-browser check @e1` / `uncheck @e1` — Toggle checkboxes +- `agent-browser close` — Close browser +- Re-snapshot after page changes to get updated refs diff --git a/README.md b/README.md index 71eaa9b4..50cf4845 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,11 @@ [![Install MCP Server in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=Neon&config=eyJ1cmwiOiJodHRwczovL21jcC5uZW9uLnRlY2gvbWNwIn0%3D) -**Neon MCP Server** is an open-source tool that lets you interact with your Neon Postgres databases in **natural language**. - [![npm version](https://img.shields.io/npm/v/@neondatabase/mcp-server-neon)](https://www.npmjs.com/package/@neondatabase/mcp-server-neon) [![npm downloads](https://img.shields.io/npm/dt/@neondatabase/mcp-server-neon)](https://www.npmjs.com/package/@neondatabase/mcp-server-neon) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -The Model Context Protocol (MCP) is a [standardized protocol](https://modelcontextprotocol.io/introduction) designed to manage context between large language models (LLMs) and external systems. This repository offers an installer and an MCP Server for [Neon](https://neon.tech). +The **Neon MCP Server** lets you connect your Neon Postgres databases with agentic tools like Claude, Cursor, and GitHub Copilot. The Model Context Protocol (MCP) is a [standardized protocol](https://modelcontextprotocol.io/introduction) designed to manage context between large language models (LLMs) and external systems. This repository offers an MCP Server for [Neon](https://neon.com). Neon's MCP server acts as a bridge between natural language requests and the [Neon API](https://api-docs.neon.tech/reference/getting-started-with-neon-api). Built upon MCP, it translates your requests into the necessary API calls, enabling you to manage tasks such as creating projects and branches, running queries, and performing database migrations seamlessly. @@ -22,7 +20,7 @@ Some of the key features of the Neon MCP server include: - **Natural language interaction:** Manage Neon databases using intuitive, conversational commands. - **Simplified database management:** Perform complex actions without writing SQL or directly using the Neon API. -- **Accessibility for non-developers:** Empower users with varying technical backgrounds to interact with Neon databases. +- **Fine-grained access control:** Choose a permission preset, scope to a single project, and protect production branches. - **Database migration support:** Leverage Neon's branching capabilities for database schema changes initiated via natural language. For example, in Claude Code, or any MCP Client, you can use natural language to accomplish things with Neon, such as: @@ -31,13 +29,15 @@ For example, in Claude Code, or any MCP Client, you can use natural language to - `I want to run a migration on my project called "my-project" that alters the users table to add a new column called "created_at".` - `Can you give me a summary of all of my Neon projects and what data is in each one?` -> [!WARNING] -> **Neon MCP Server Security Considerations** +> [!WARNING] +> **Neon MCP Server Security Considerations** > The Neon MCP Server grants powerful database management capabilities through natural language requests. **Always review and authorize actions requested by the LLM before execution.** Ensure that only authorized users and applications have access to the Neon MCP Server. > -> The Neon MCP Server is intended for local development and IDE integrations only. **We do not recommend using the Neon MCP Server in production environments.** It can execute powerful operations that may lead to accidental or unauthorized changes. +> Consider using [permission presets](#permission-presets) and [production branch protection](#production-branch-protection) to limit the blast radius of AI-initiated operations. > -> For more information, see [MCP security guidance →](https://neon.tech/docs/ai/neon-mcp-server#mcp-security-guidance). +> For more information, see [MCP security guidance](https://neon.tech/docs/ai/neon-mcp-server#mcp-security-guidance). + +For full documentation, visit [neon.com/docs/ai/neon-mcp-server](https://neon.com/docs/ai/neon-mcp-server). The docs include an interactive configurator that lets you select your preferred permission preset, project scoping, and branch protection options and generates the correct MCP client configuration for you. ## Setting up Neon MCP Server @@ -48,6 +48,8 @@ There are a few options for setting up the Neon MCP Server: 3. **Remote MCP Server (API Key Based Authentication):** Connect to Neon's managed MCP server using API key for authentication. This method is useful if you want to connect a remote agent to Neon where OAuth is not available. Additionally, you will automatically receive the latest features and improvements as soon as they are released. 4. **Local MCP Server:** Run the Neon MCP server locally on your machine, authenticating with a Neon API key. +All options support [permission presets](#permission-presets), [project scoping](#project-scoping), and [production branch protection](#production-branch-protection). + ### Prerequisites - An MCP Client application. @@ -126,6 +128,8 @@ Alternatively, you can add the following "Neon" entry to your client's MCP serve > Provide an organization's API key to limit access to projects under the organization only. +> MCP supports two remote server transports: the deprecated Server-Sent Events (SSE) and the newer, recommended Streamable HTTP. If your LLM client doesn't support Streamable HTTP yet, you can switch the endpoint from `https://mcp.neon.tech/mcp` to `https://mcp.neon.tech/sse` to use SSE instead. + ### Option 4. Local MCP Server Run the Neon MCP server on your local machine with your Neon API key. This method allows you to manage your Neon projects and databases without relying on a remote MCP server. @@ -148,30 +152,176 @@ Run the Neon MCP server on your local machine with your Neon API key. This metho } ``` -### Read-Only Mode +### Server-Sent Events (SSE) Transport (Deprecated) + +MCP supports two remote server transports: the deprecated Server-Sent Events (SSE) and the newer, recommended Streamable HTTP. If your LLM client doesn't support Streamable HTTP yet, you can switch the endpoint from `https://mcp.neon.tech/mcp` to `https://mcp.neon.tech/sse` to use SSE instead. -**Read-Only Mode:** Restricts which tools are available, disabling write operations like creating projects, branches, or running migrations. Read-only tools include listing projects, describing schemas, querying data, and viewing performance metrics. +Run the following command to add the Neon MCP Server for all detected agents and editors in your workspace using the SSE transport: + +```bash +npx add-mcp https://mcp.neon.tech/sse --type sse +``` + +### Troubleshooting + +If your client does not use `JSON` for configuration of MCP servers (such as older versions of Cursor), you can use the following command when prompted: + +```bash +npx -y @neondatabase/mcp-server-neon start +``` -You can enable read-only mode in two ways: +#### Troubleshooting on Windows -1. **OAuth Scope Selection (Recommended):** When connecting via OAuth, uncheck "Full access" during authorization to operate in read-only mode. -2. **Header Override:** Add the `x-read-only` header to your configuration: +If you are using Windows and encounter issues while adding the MCP server, you might need to use the Command Prompt (`cmd`) or Windows Subsystem for Linux (`wsl`) to run the necessary commands. Your configuration setup may resemble the following: ```json { "mcpServers": { - "Neon": { - "url": "https://mcp.neon.tech/mcp", - "headers": { - "x-read-only": "true" - } + "neon": { + "command": "cmd", + "args": [ + "/c", + "npx", + "-y", + "@neondatabase/mcp-server-neon", + "start", + "" + ] + } + } +} +``` + +```json +{ + "mcpServers": { + "neon": { + "command": "wsl", + "args": [ + "npx", + "-y", + "@neondatabase/mcp-server-neon", + "start", + "" + ] } } } ``` +## Guides + +- [Neon MCP Server Guide](https://neon.com/docs/ai/neon-mcp-server) (includes an interactive configurator for scopes and presets) +- [Connect MCP Clients to Neon](https://neon.com/docs/ai/connect-mcp-clients-to-neon) +- [Cursor with Neon MCP Server](https://neon.tech/guides/cursor-mcp-neon) +- [Claude Desktop with Neon MCP Server](https://neon.tech/guides/neon-mcp-server) +- [Cline with Neon MCP Server](https://neon.tech/guides/cline-mcp-neon) +- [Windsurf with Neon MCP Server](https://neon.tech/guides/windsurf-mcp-neon) +- [Zed with Neon MCP Server](https://neon.tech/guides/zed-mcp-neon) + +# Fine-Grained Access Control + +The Neon MCP Server supports fine-grained access control through **permission presets**, **project scoping**, **production branch protection**, and **custom scope categories**. These controls let you limit what an AI agent can do, reducing the blast radius of potential mistakes. + +Access control can be configured via: + +- **HTTP headers** (`X-Neon-*`) for the remote MCP server with API key authentication +- **CLI flags** (`--preset`, `--project-id`, etc.) for the local MCP server +- **OAuth consent UI** when connecting via OAuth (the authorization page lets you select a preset, project scope, and branch protection) + +> **OAuth vs API key:** When using OAuth, access control settings are locked in during the consent flow and stored with the token. HTTP headers (`X-Neon-*`) are ignored for OAuth-authenticated requests -- to change permissions, re-authenticate through a new OAuth flow. HTTP headers only apply when authenticating with an API key via the `Authorization` header. + +## Access Control Reference + +### HTTP Headers (Remote MCP Server with API Key) + +| Header | Values | Default | Description | +| --------------------------- | ------------------------------------------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------- | +| `X-Neon-Preset` | `full_access`, `local_development`, `production_use`, `custom` | `full_access` | Permission preset. Invalid values silently fall back to `full_access`. | +| `X-Neon-Scopes` | Comma-separated: `projects`, `branches`, `schema`, `querying`, `performance`, `neon_auth`, `docs` | Not set | Custom scope categories. **Overrides preset to `custom`** when present. Invalid category names are silently dropped. | +| `X-Neon-Project-Id` | Neon project ID (e.g., `proj-abc-123`) | Not set | Scope all operations to a single project. | +| `X-Neon-Protect-Production` | `true`, `false`, branch name, or comma-separated names | Not set | Protect branches from destructive operations. `true` protects `main`, `master`, `prod`, `production`. | +| `X-Neon-Read-Only` | `true` / `false` | Not set | Explicit read-only mode (highest priority). The legacy `x-read-only` header is accepted as a synonym with lower priority. | + +### CLI Flags (Local MCP Server) + +| Flag | Values | Default | Description | +| ---------------------- | ------------------------------------------------------------------------------------------------- | ------------- | ------------------------------------------------------ | +| `--preset` | `full_access`, `local_development`, `production_use`, `custom` | `full_access` | Permission preset | +| `--scopes` | Comma-separated: `projects`, `branches`, `schema`, `querying`, `performance`, `neon_auth`, `docs` | Not set | Custom scope categories (overrides preset to `custom`) | +| `--project-id` | Neon project ID | Not set | Scope all operations to a single project | +| `--protect-production` | `true` (no value), branch name, or comma-separated names | Not set | Protect branches from destructive operations | + +### Precedence Rules + +1. `X-Neon-Read-Only` / `x-read-only` header takes the highest priority for read-only determination +2. `X-Neon-Scopes` / `--scopes` always overrides `X-Neon-Preset` / `--preset` (implies `custom` preset) +3. `production_use` preset implies read-only mode +4. When no access control headers or flags are provided, the default is `full_access` with no project scoping and no branch protection + +In **OAuth mode**, all access control is determined during the consent flow and stored with the token. HTTP headers are ignored for OAuth-authenticated requests. + +## Permission Presets + +Presets are predefined access levels that control which tools are available. Set via `X-Neon-Preset` header or `--preset` CLI flag. The default is `full_access`. + +| Preset | Description | Write access | Project create/delete | SQL execution | +| ------------------- | ------------------------------------------------------------------------------------------ | ----------------- | --------------------- | ----------------- | +| `full_access` | No restrictions. Use with caution. | Yes | Yes | Yes | +| `local_development` | Safe development access with branch management and SQL. | Yes | No | Yes | +| `production_use` | Read-only access for schema inspection and documentation. | No | No | Read-only | +| `custom` | Select specific tool categories to enable (see [Custom Scopes](#custom-scope-categories)). | Depends on scopes | Depends on scopes | Depends on scopes | + +## Project Scoping + +Restrict the MCP server to a single Neon project using `X-Neon-Project-Id` header or `--project-id` CLI flag. When project-scoped: + +- **Project-agnostic tools are hidden** (`list_projects`, `create_project`, `delete_project`, `list_organizations`, `list_shared_projects`) +- **`projectId` is auto-injected** into all tool calls and removed from schemas visible to the LLM +- The LLM cannot accidentally operate on the wrong project + +## Production Branch Protection + +Protect critical branches from destructive operations (`delete_branch`, `reset_from_parent`, `run_sql`, `run_sql_transaction`) using `X-Neon-Protect-Production` header or `--protect-production` CLI flag. + +| Value | Behavior | +| ------------------- | ------------------------------------------------------------------ | +| `true` | Protects branches named `main`, `master`, `prod`, and `production` | +| `my-branch` | Protects the single branch named `my-branch` | +| `main,staging,prod` | Protects all listed branches (comma-separated) | + +When connecting via OAuth, check the "Protect production branches" checkbox in the authorization dialog. + +## Custom Scope Categories + +For fine-grained control beyond presets, enable specific tool categories using `X-Neon-Scopes` header or `--scopes` CLI flag. When scopes are provided, the preset is automatically set to `custom` regardless of any preset value. + +| Category | Tools included | +| ------------- | ------------------------------------------------------------------------------------------------------------------------- | +| `projects` | `list_projects`, `create_project`, `delete_project`, `describe_project`, `list_organizations`, `list_shared_projects` | +| `branches` | `create_branch`, `delete_branch`, `describe_branch`, `reset_from_parent`, `list_branch_computes`, `get_connection_string` | +| `schema` | `get_database_tables`, `describe_table_schema` | +| `querying` | `run_sql`, `run_sql_transaction`, `prepare_database_migration`, `complete_database_migration`, `compare_database_schema` | +| `performance` | `explain_sql_statement`, `prepare_query_tuning`, `complete_query_tuning`, `list_slow_queries` | +| `neon_auth` | `provision_neon_auth`, `provision_neon_data_api` | +| `docs` | `load_resource` | + +The `search` and `fetch` tools are always available regardless of scope selection. At least one valid scope category is required for useful access. Values are case-sensitive. + +## Read-Only Mode + +Read-only mode restricts which tools are available, disabling write operations like creating projects, branches, or running migrations. Enable it via: + +1. **`X-Neon-Read-Only: true` header** -- highest priority, overrides all other settings +2. **`production_use` preset** -- automatically enables read-only mode +3. **OAuth scope selection** -- uncheck "Full access" during authorization + +Read-only mode is useful in combination with custom scopes: scopes control **which** tool categories are available, while read-only controls **whether** write tools within those categories are included. + > **Note:** Read-only mode restricts which _tools_ are available, not the SQL content. The `run_sql` tool remains available and can execute any SQL including INSERT/UPDATE/DELETE. For true read-only SQL access, use database roles with restricted permissions. +> The legacy `x-read-only` header is accepted as a synonym for `X-Neon-Read-Only`. If both are present, `X-Neon-Read-Only` takes priority. +
Tools available in read-only mode @@ -192,80 +342,88 @@ You can enable read-only mode in two ways:
-### Server-Sent Events (SSE) Transport (Deprecated) +## Examples -MCP supports two remote server transports: the deprecated Server-Sent Events (SSE) and the newer, recommended Streamable HTTP. If your LLM client doesn't support Streamable HTTP yet, you can switch the endpoint from `https://mcp.neon.tech/mcp` to `https://mcp.neon.tech/sse` to use SSE instead. +All access control options can be combined. Here are common configurations: -Run the following command to add the Neon MCP Server for all detected agents and editors in your workspace using the SSE transport: +### Safe Development Environment (Remote) + +Scope to a single project, allow development operations, and protect production branches: ```bash -npx add-mcp https://mcp.neon.tech/sse --type sse +npx add-mcp@latest https://mcp.neon.tech/mcp \ + --name Neon \ + --header "Authorization: Bearer $NEON_API_KEY" \ + --header "X-Neon-Preset: local_development" \ + --header "X-Neon-Project-Id: my-project-abc123" \ + --header "X-Neon-Protect-Production: true" ``` -### Troubleshooting - -If your client does not use `JSON` for configuration of MCP servers (such as older versions of Cursor), you can use the following command when prompted: - -```bash -npx -y @neondatabase/mcp-server-neon start +```json +{ + "mcpServers": { + "Neon": { + "url": "https://mcp.neon.tech/mcp", + "headers": { + "Authorization": "Bearer ", + "X-Neon-Preset": "local_development", + "X-Neon-Project-Id": "", + "X-Neon-Protect-Production": "true" + } + } + } +} ``` -#### Troubleshooting on Windows +### Read-Only Schema Inspector (Remote) -If you are using Windows and encounter issues while adding the MCP server, you might need to use the Command Prompt (`cmd`) or Windows Subsystem for Linux (`wsl`) to run the necessary commands. Your configuration setup may resemble the following: +Only allow schema and documentation access on a single project: ```json { "mcpServers": { - "neon": { - "command": "cmd", - "args": [ - "/c", - "npx", - "-y", - "@neondatabase/mcp-server-neon", - "start", - "" - ] + "Neon": { + "url": "https://mcp.neon.tech/mcp", + "headers": { + "Authorization": "Bearer ", + "X-Neon-Scopes": "schema,docs", + "X-Neon-Project-Id": "" + } } } } ``` +### Safe Development Environment (Local) + ```json { "mcpServers": { "neon": { - "command": "wsl", + "command": "npx", "args": [ - "npx", "-y", "@neondatabase/mcp-server-neon", "start", - "" + "", + "--preset", + "local_development", + "--project-id", + "", + "--protect-production" ] } } } ``` -## Guides - -- [Neon MCP Server Guide](https://neon.tech/docs/ai/neon-mcp-server) -- [Connect MCP Clients to Neon](https://neon.tech/docs/ai/connect-mcp-clients-to-neon) -- [Cursor with Neon MCP Server](https://neon.tech/guides/cursor-mcp-neon) -- [Claude Desktop with Neon MCP Server](https://neon.tech/guides/neon-mcp-server) -- [Cline with Neon MCP Server](https://neon.tech/guides/cline-mcp-neon) -- [Windsurf with Neon MCP Server](https://neon.tech/guides/windsurf-mcp-neon) -- [Zed with Neon MCP Server](https://neon.tech/guides/zed-mcp-neon) - # Features ## Supported Tools The Neon MCP Server provides the following actions, which are exposed as "tools" to MCP Clients. You can use these tools to interact with your Neon projects and databases using natural language commands. -**Project Management:** +**Project Management** (scope: `projects`): - **`list_projects`**: Lists the first 10 Neon projects in your account, providing a summary of each project. If you can't find a specific project, increase the limit by passing a higher value to the `limit` parameter. - **`list_shared_projects`**: Lists Neon projects shared with the current user. Supports a search parameter and limiting the number of projects returned (default: 10). @@ -274,49 +432,46 @@ The Neon MCP Server provides the following actions, which are exposed as "tools" - **`delete_project`**: Deletes an existing Neon project and all its associated resources. - **`list_organizations`**: Lists all organizations that the current user has access to. Optionally filter by organization name or ID using the search parameter. -**Branch Management:** +**Branch Management** (scope: `branches`): - **`create_branch`**: Creates a new branch within a specified Neon project. Leverages [Neon's branching](/docs/introduction/branching) feature for development, testing, or migrations. - **`delete_branch`**: Deletes an existing branch from a Neon project. - **`describe_branch`**: Retrieves details about a specific branch, such as its name, ID, and parent branch. - **`list_branch_computes`**: Lists compute endpoints for a project or specific branch, including compute ID, type, size, last active time, and autoscaling information. -- **`compare_database_schema`**: Shows the schema diff between the child branch and its parent +- **`get_connection_string`**: Returns your database connection string. - **`reset_from_parent`**: Resets the current branch to its parent's state, discarding local changes. Automatically preserves to backup if branch has children, or optionally preserve on request with a custom name. -**SQL Query Execution:** +**Schema and Table Inspection** (scope: `schema`): -- **`get_connection_string`**: Returns your database connection string. -- **`run_sql`**: Executes a single SQL query against a specified Neon database. Supports both read and write operations. -- **`run_sql_transaction`**: Executes a series of SQL queries within a single transaction against a Neon database. - **`get_database_tables`**: Lists all tables within a specified Neon database. - **`describe_table_schema`**: Retrieves the schema definition of a specific table, detailing columns, data types, and constraints. -**Database Migrations (Schema Changes):** +**SQL Query Execution and Migrations** (scope: `querying`): +- **`run_sql`**: Executes a single SQL query against a specified Neon database. Supports both read and write operations. +- **`run_sql_transaction`**: Executes a series of SQL queries within a single transaction against a Neon database. - **`prepare_database_migration`**: Initiates a database migration process. Critically, it creates a temporary branch to apply and test the migration safely before affecting the main branch. - **`complete_database_migration`**: Finalizes and applies a prepared database migration to the main branch. This action merges changes from the temporary migration branch and cleans up temporary resources. +- **`compare_database_schema`**: Shows the schema diff between the child branch and its parent. -**Query Performance Optimization:** +**Query Performance Optimization** (scope: `performance`): - **`list_slow_queries`**: Identifies performance bottlenecks by finding the slowest queries in a database. Requires the pg_stat_statements extension. - **`explain_sql_statement`**: Provides detailed execution plans for SQL queries to help identify performance bottlenecks. - **`prepare_query_tuning`**: Analyzes query performance and suggests optimizations, like index creation. Creates a temporary branch for safely testing these optimizations. - **`complete_query_tuning`**: Finalizes query tuning by either applying optimizations to the main branch or discarding them. Cleans up the temporary tuning branch. -**Neon Auth:** +**Neon Auth** (scope: `neon_auth`): - **`provision_neon_auth`**: Provisions Neon Auth for a Neon project. It allows developers to easily set up authentication infrastructure by creating an integration with an Auth provider. - -**Neon Data API:** - - **`provision_neon_data_api`**: Provisions the Neon Data API for HTTP-based database access with optional JWT authentication via Neon Auth or external JWKS providers. -**Search and Discovery:** +**Search and Discovery** (always available): - **`search`**: Searches across organizations, projects, and branches matching a query. Returns IDs, titles, and direct links to the Neon Console. - **`fetch`**: Fetches detailed information about a specific organization, project, or branch using an ID (typically from the search tool). -**Documentation and Resources:** +**Documentation and Resources** (scope: `docs`): - **`load_resource`**: Loads comprehensive Neon documentation and usage guidelines, including the "neon-get-started" guide for setup, configuration, and best practices. @@ -350,6 +505,9 @@ bun run build:cli # Run the CLI locally bun run start:cli $NEON_API_KEY + +# Run the CLI with access control flags +bun run start:cli $NEON_API_KEY -- --preset local_development --project-id my-project --protect-production ``` ## Linting and Type Checking diff --git a/dev-notes/read-only-mode-fix.md b/dev-notes/read-only-mode-fix.md deleted file mode 100644 index d498d3f2..00000000 --- a/dev-notes/read-only-mode-fix.md +++ /dev/null @@ -1,197 +0,0 @@ -# Read-Only Mode Fix - -**Date:** 2026-01-14 -**Commit:** 7bb3e65 -**Branch:** fix-read-only-mode -**Category:** security / access-control - ---- - -## Problem Summary - -The MCP server's read-only mode was not being properly determined from OAuth scopes or request headers. The `readOnly` flag was always hardcoded to `false` in the `verifyToken` function, making the read-only filtering logic in the server ineffective. - -## Symptoms - -- Users with OAuth tokens containing only the `read` scope could still access write-only tools (e.g., `create_project`, `delete_branch`) -- The `X-READ-ONLY` header had no effect on tool availability -- The `scopes_supported` field was missing from the OAuth authorization server metadata - -## Root Cause - -In `landing/app/api/[transport]/route.ts`, the `verifyToken` function was returning `readOnly: false` in both code paths (OAuth tokens and API keys): - -```typescript -// OAuth path - hardcoded to false -return { - // ... - extra: { - // ... - readOnly: false, // BUG: Should be determined from scope - }, -}; - -// API key path - hardcoded to false -return { - // ... - extra: { - // ... - readOnly: false, // BUG: Should be determined from header - }, -}; -``` - -The server did have proper filtering logic in `landing/mcp-src/server/index.ts`: - -```typescript -const availableTools = context.readOnly - ? NEON_TOOLS.filter((tool) => tool.readOnlySafe) - : NEON_TOOLS; -``` - -But this logic never activated because `context.readOnly` was always `false`. - -## Solution - -### 1. Created `landing/mcp-src/utils/read-only.ts` - -A new utility module with clear priority rules: - -```typescript -export const SUPPORTED_SCOPES = ['read', 'write', '*'] as const; - -export type ReadOnlyContext = { - headerValue?: string | null; - scope?: string | string[] | null; -}; - -/** - * Determines if the request should operate in read-only mode. - * Priority: X-READ-ONLY header > OAuth scope > default (false) - */ -export function isReadOnly(context: ReadOnlyContext): boolean { - // 1. Check X-READ-ONLY header first (explicit override) - const headerResult = parseReadOnlyHeader(context.headerValue); - if (headerResult !== undefined) { - return headerResult; - } - - // 2. Check OAuth scope (only 'read' scope = read-only) - if (context.scope !== undefined && context.scope !== null) { - return isScopeReadOnly(context.scope); - } - - // 3. Default to read-write access - return false; -} -``` - -Key design decisions: -- **Priority order:** Header > Scope > Default - allows explicit override via header -- **Scope parsing:** Handles both string (`"read write"`) and array (`["read", "write"]`) formats -- **Read-only determination:** Only read-only if scope contains ONLY `read` (no `write`, no `*`) - -### 2. Updated `landing/app/api/[transport]/route.ts` - -Modified `verifyToken` to use the new utility: - -```typescript -const readOnlyHeader = req.headers.get('x-read-only'); - -// OAuth path -const readOnly = isReadOnly({ - headerValue: readOnlyHeader, - scope: token.scope, -}); - -// API key path (no OAuth scopes) -const readOnly = isReadOnly({ - headerValue: readOnlyHeader, -}); -``` - -### 3. Updated OAuth metadata - -Added `scopes_supported` to `landing/app/.well-known/oauth-authorization-server/route.ts`: - -```typescript -scopes_supported: SUPPORTED_SCOPES, -``` - -This allows clients to discover what scopes the server supports. - -## Files Changed - -| File | Change | -|------|--------| -| `landing/mcp-src/utils/read-only.ts` | **New file** - Read-only mode detection logic | -| `landing/app/api/[transport]/route.ts` | Use `isReadOnly()` for both OAuth and API key paths | -| `landing/app/.well-known/oauth-authorization-server/route.ts` | Add `scopes_supported` to metadata | - -## Testing - -### Manual Testing - -1. **OAuth with `read` scope only:** - ```bash - # Should only see read-only tools - curl -H "Authorization: Bearer " https://mcp.neon.tech/api/mcp - ``` - -2. **X-READ-ONLY header:** - ```bash - # Force read-only mode even with full-access token - curl -H "Authorization: Bearer " \ - -H "X-READ-ONLY: true" \ - https://mcp.neon.tech/api/mcp - ``` - -3. **API key with header override:** - ```bash - # API keys default to read-write, but header can restrict - curl -H "Authorization: Bearer " \ - -H "X-READ-ONLY: true" \ - https://mcp.neon.tech/api/mcp - ``` - -### Verification - -Check that read-only mode properly filters tools: -- `list_projects` (readOnlySafe: true) - Should be available -- `create_project` (readOnlySafe: false) - Should NOT be available in read-only mode - -## Prevention - -### Design Pattern for Future Features - -When adding new authentication or authorization features: - -1. **Don't hardcode access levels** - Always derive from actual auth context -2. **Create dedicated utility modules** - Centralize access control logic -3. **Document priority rules** - Make override behavior explicit -4. **Test all auth paths** - OAuth tokens AND API keys - -### Code Review Checklist - -- [ ] Search for hardcoded `readOnly: false` or similar access flags -- [ ] Verify all auth paths use the same access determination logic -- [ ] Check OAuth metadata exposes supported scopes/capabilities -- [ ] Test header overrides work correctly - -## Related Documentation - -- **CLAUDE.md** - Documents the read-only mode design: - > Read-Only Mode (`landing/mcp-src/utils/read-only.ts`): Tools define a `readOnlySafe` property. When the server runs in read-only mode, only tools marked as `readOnlySafe: true` are available. - -- **Tool definitions** - Each tool in `landing/mcp-src/tools/definitions.ts` has: - - `readOnlySafe: boolean` - Whether the tool is safe for read-only mode - - `annotations.readOnlyHint` - MCP standard hint for clients - -## Summary - -| Aspect | Before | After | -|--------|--------|-------| -| OAuth scope detection | Ignored | Properly reads `token.scope` | -| X-READ-ONLY header | Ignored | Highest priority override | -| OAuth metadata | Missing scopes | Includes `scopes_supported` | -| Tool filtering | Always all tools | Filtered by `readOnlySafe` | diff --git a/landing/.gitignore b/landing/.gitignore index 86c08d70..bcca84dd 100644 --- a/landing/.gitignore +++ b/landing/.gitignore @@ -12,6 +12,8 @@ # testing /coverage +/test-results/ +/playwright-report/ # next.js /.next/ diff --git a/landing/README.md b/landing/README.md index ba23b5d6..ce9377f9 100644 --- a/landing/README.md +++ b/landing/README.md @@ -1,6 +1,7 @@ # Neon MCP Server - Landing & Remote Server This directory contains: + 1. **Landing Page**: Marketing site for the Neon MCP Server 2. **Remote MCP Server**: Vercel-hosted serverless MCP server accessible at `mcp.neon.tech` @@ -32,6 +33,7 @@ The server supports read-only mode to restrict available tools. Priority for det Available scopes: `read`, `write`, `*` During OAuth authorization, users see a permissions dialog where they can: + - **Read-only**: Always granted (view projects, run queries) - **Full access**: Optional checkbox (create/delete resources, migrations) diff --git a/landing/app/.well-known/oauth-authorization-server/route.ts b/landing/app/.well-known/oauth-authorization-server/route.ts index cb0dcade..c93cdfbe 100644 --- a/landing/app/.well-known/oauth-authorization-server/route.ts +++ b/landing/app/.well-known/oauth-authorization-server/route.ts @@ -1,15 +1,16 @@ -import { NextResponse } from 'next/server'; -import { SERVER_HOST } from '@/lib/config'; -import { SUPPORTED_SCOPES } from '@/mcp-src/utils/read-only'; +import { NextResponse } from "next/server"; +import { SERVER_HOST } from "@/lib/config"; +import { SUPPORTED_SCOPES } from "@/mcp-src/utils/read-only"; +import { SCOPE_CATEGORIES, PRESETS } from "@/mcp-src/utils/grant-context"; -const SUPPORTED_GRANT_TYPES = ['authorization_code', 'refresh_token']; -const SUPPORTED_RESPONSE_TYPES = ['code']; +const SUPPORTED_GRANT_TYPES = ["authorization_code", "refresh_token"]; +const SUPPORTED_RESPONSE_TYPES = ["code"]; const SUPPORTED_AUTH_METHODS = [ - 'client_secret_post', - 'client_secret_basic', - 'none', + "client_secret_post", + "client_secret_basic", + "none", ]; -const SUPPORTED_CODE_CHALLENGE_METHODS = ['S256']; +const SUPPORTED_CODE_CHALLENGE_METHODS = ["S256"]; export async function GET() { return NextResponse.json({ @@ -19,13 +20,15 @@ export async function GET() { registration_endpoint: `${SERVER_HOST}/api/register`, revocation_endpoint: `${SERVER_HOST}/api/revoke`, response_types_supported: SUPPORTED_RESPONSE_TYPES, - response_modes_supported: ['query'], + response_modes_supported: ["query"], grant_types_supported: SUPPORTED_GRANT_TYPES, token_endpoint_auth_methods_supported: SUPPORTED_AUTH_METHODS, registration_endpoint_auth_methods_supported: SUPPORTED_AUTH_METHODS, revocation_endpoint_auth_methods_supported: SUPPORTED_AUTH_METHODS, code_challenge_methods_supported: SUPPORTED_CODE_CHALLENGE_METHODS, scopes_supported: SUPPORTED_SCOPES, + "x-neon-presets": PRESETS, + "x-neon-scope-categories": SCOPE_CATEGORIES, }); } @@ -33,9 +36,9 @@ export async function OPTIONS() { return new NextResponse(null, { status: 204, headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", }, }); } diff --git a/landing/app/.well-known/oauth-protected-resource/route.ts b/landing/app/.well-known/oauth-protected-resource/route.ts index b35881bd..c1079e0c 100644 --- a/landing/app/.well-known/oauth-protected-resource/route.ts +++ b/landing/app/.well-known/oauth-protected-resource/route.ts @@ -1,12 +1,12 @@ -import { NextResponse } from 'next/server'; -import { SERVER_HOST } from '@/lib/config'; +import { NextResponse } from "next/server"; +import { SERVER_HOST } from "@/lib/config"; export async function GET() { return NextResponse.json({ resource: SERVER_HOST, authorization_servers: [SERVER_HOST], - bearer_methods_supported: ['header'], - resource_documentation: 'https://neon.tech/docs/mcp', + bearer_methods_supported: ["header"], + resource_documentation: "https://neon.tech/docs/mcp", }); } @@ -14,9 +14,9 @@ export async function OPTIONS() { return new NextResponse(null, { status: 204, headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", }, }); } diff --git a/landing/app/api/[transport]/route.ts b/landing/app/api/[transport]/route.ts index a10f7a8a..a92d7e9f 100644 --- a/landing/app/api/[transport]/route.ts +++ b/landing/app/api/[transport]/route.ts @@ -1,39 +1,56 @@ // Initialize Sentry (must be first import) -import '../../../mcp-src/sentry/instrument'; - -import type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js'; -import { createMcpHandler, withMcpAuth } from 'mcp-handler'; -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { captureException, startSpan } from '@sentry/node'; - -import { NEON_RESOURCES } from '../../../mcp-src/resources'; -import { NEON_PROMPTS, getPromptTemplate } from '../../../mcp-src/prompts'; -import { NEON_HANDLERS, NEON_TOOLS } from '../../../mcp-src/tools/index'; -import { createNeonClient } from '../../../mcp-src/server/api'; -import pkg from '../../../package.json'; -import { handleToolError } from '../../../mcp-src/server/errors'; -import type { ToolHandlerExtraParams } from '../../../mcp-src/tools/types'; -import { detectClientApplication } from '../../../mcp-src/utils/client-application'; -import { isReadOnly } from '../../../mcp-src/utils/read-only'; -import type { AuthContext } from '../../../mcp-src/types/auth'; -import { logger } from '../../../mcp-src/utils/logger'; -import { generateTraceId } from '../../../mcp-src/utils/trace'; -import { waitUntil } from '@vercel/functions'; -import { track, flushAnalytics } from '../../../mcp-src/analytics/analytics'; -import { resolveAccountFromAuth } from '../../../mcp-src/server/account'; -import { model } from '../../../mcp-src/oauth/model'; -import { getApiKeys, type ApiKeyRecord } from '../../../mcp-src/oauth/kv-store'; -import { setSentryTags } from '../../../mcp-src/sentry/utils'; -import type { ServerContext, AppContext } from '../../../mcp-src/types/context'; +import "../../../mcp-src/sentry/instrument"; + +import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js"; +import { createMcpHandler, withMcpAuth } from "mcp-handler"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; +import { captureException, startSpan } from "@sentry/node"; + +import { NEON_PROMPTS, getPromptTemplate } from "../../../mcp-src/prompts"; +import { NEON_HANDLERS, NEON_TOOLS } from "../../../mcp-src/tools/index"; +import { createNeonClient } from "../../../mcp-src/server/api"; +import pkg from "../../../package.json"; +import { handleToolError } from "../../../mcp-src/server/errors"; +import type { ToolHandlerExtraParams } from "../../../mcp-src/tools/types"; +import { detectClientApplication } from "../../../mcp-src/utils/client-application"; +import { isReadOnly } from "../../../mcp-src/utils/read-only"; +import type { AuthContext } from "../../../mcp-src/types/auth"; +import { logger } from "../../../mcp-src/utils/logger"; +import { generateTraceId } from "../../../mcp-src/utils/trace"; +import { waitUntil } from "@vercel/functions"; +import { track, flushAnalytics } from "../../../mcp-src/analytics/analytics"; +import { resolveAccountFromAuth } from "../../../mcp-src/server/account"; +import { model } from "../../../mcp-src/oauth/model"; +import { getApiKeys, type ApiKeyRecord } from "../../../mcp-src/oauth/kv-store"; +import { setSentryTags } from "../../../mcp-src/sentry/utils"; +import type { ServerContext, AppContext } from "../../../mcp-src/types/context"; +import { + resolveGrantFromHeaders, + resolveGrantFromToken, + DEFAULT_GRANT, + type GrantContext, +} from "../../../mcp-src/utils/grant-context"; +import { + filterToolsForGrant, + getAvailableTools, + getAccessControlWarnings, + injectProjectId, +} from "../../../mcp-src/tools/grant-filter"; +import { + enforceProtectedBranches, + GrantViolationError, +} from "../../../mcp-src/tools/grant-enforcement"; type AuthenticatedExtra = { authInfo?: AuthInfo & { extra?: { apiKey?: string; - account?: AuthContext['extra']['account']; + account?: AuthContext["extra"]["account"]; readOnly?: boolean; - client?: AuthContext['extra']['client']; - transport?: AppContext['transport']; + grant?: GrantContext; + client?: AuthContext["extra"]["client"]; + transport?: AppContext["transport"]; userAgent?: string; }; }; @@ -45,17 +62,17 @@ type AuthenticatedExtra = { const handler = createMcpHandler( (server: McpServer) => { // Request-scoped mutable state (isolated per server instance) - let clientName = 'unknown'; + let clientName = "unknown"; let clientApplication = detectClientApplication(clientName); let hasTrackedServerInit = false; let lastKnownContext: ServerContext | undefined; // Default app context for analytics/Sentry (used in onerror fallback) const defaultAppContext: AppContext = { - name: 'mcp-server-neon', - transport: 'sse', + name: "mcp-server-neon", + transport: "sse", environment: (process.env.NODE_ENV ?? - 'production') as AppContext['environment'], + "production") as AppContext["environment"], version: pkg.version, }; @@ -64,15 +81,20 @@ const handler = createMcpHandler( if (hasTrackedServerInit) return; hasTrackedServerInit = true; + const grant = context.grant ?? DEFAULT_GRANT; const properties = { clientName, clientApplication, readOnly: String(context.readOnly ?? false), + preset: grant.preset, + projectScoped: String(!!grant.projectId), + protectedBranches: grant.protectedBranches?.join(",") ?? "none", + customScopes: grant.scopes?.join(",") ?? "all", }; track({ userId: context.account.id, - event: 'server_init', + event: "server_init", properties, context: { client: context.client, @@ -80,39 +102,53 @@ const handler = createMcpHandler( }, }); waitUntil(flushAnalytics()); - logger.info('Server initialized:', { + logger.info("Server initialized:", { clientName, clientApplication, readOnly: context.readOnly, + grant, }); } // Helper function to get Neon client and context from auth info - function getAuthContext(extra: AuthenticatedExtra) { + async function getAuthContext(extra: AuthenticatedExtra) { const authInfo = extra.authInfo; if (!authInfo?.extra?.apiKey || !authInfo?.extra?.account) { - throw new Error('Authentication required'); + throw new Error("Authentication required"); } const apiKey = authInfo.extra.apiKey; const account = authInfo.extra.account; const readOnly = authInfo.extra.readOnly ?? false; + const grant = { ...(authInfo.extra.grant ?? DEFAULT_GRANT) }; const client = authInfo.extra.client; - const transport = authInfo.extra.transport ?? 'sse'; + const transport = authInfo.extra.transport ?? "sse"; const neonClient = createNeonClient(apiKey); + // Validate project ID against the Neon API if one was provided + if (grant.projectId) { + try { + await neonClient.getProject(grant.projectId); + } catch { + logger.warn( + `Project ID "${grant.projectId}" could not be verified — falling back to unscoped access.`, + ); + grant.projectId = null; + } + } + // Use User-Agent as clientName fallback if MCP handshake hasn't provided it yet - if (clientName === 'unknown' && authInfo.extra.userAgent) { + if (clientName === "unknown" && authInfo.extra.userAgent) { clientName = authInfo.extra.userAgent; clientApplication = detectClientApplication(clientName); } // Create dynamic appContext with actual transport const dynamicAppContext: AppContext = { - name: 'mcp-server-neon', + name: "mcp-server-neon", transport, environment: (process.env.NODE_ENV ?? - 'production') as AppContext['environment'], + "production") as AppContext["environment"], version: pkg.version, }; @@ -123,6 +159,7 @@ const handler = createMcpHandler( app: dynamicAppContext, readOnly, client, + grant, }; lastKnownContext = context; @@ -130,6 +167,7 @@ const handler = createMcpHandler( apiKey, account, readOnly, + grant, neonClient, clientApplication, clientName, @@ -141,7 +179,7 @@ const handler = createMcpHandler( // Set up lifecycle hooks for client detection and error handling server.server.oninitialized = () => { const clientInfo = server.server.getClientVersion(); - logger.info('MCP oninitialized:', { + logger.info("MCP oninitialized:", { clientInfo, hasName: !!clientInfo?.name, currentClientName: clientName, @@ -158,14 +196,14 @@ const handler = createMcpHandler( }; server.server.onerror = (error: unknown) => { - const message = error instanceof Error ? error.message : 'Unknown error'; - logger.error('Server error:', { + const message = error instanceof Error ? error.message : "Unknown error"; + logger.error("Server error:", { message, error, }); // Use last known context if available, otherwise use defaults - const userId = lastKnownContext?.account?.id ?? 'unknown'; + const userId = lastKnownContext?.account?.id ?? "unknown"; const contexts = { app: lastKnownContext?.app ?? defaultAppContext, client: lastKnownContext?.client, @@ -180,7 +218,7 @@ const handler = createMcpHandler( track({ userId, - event: 'server_error', + event: "server_error", properties: { message, error, eventId }, context: contexts, }); @@ -204,12 +242,13 @@ const handler = createMcpHandler( const { account, readOnly, + grant, neonClient, clientApplication: clientApp, clientName: cName, client, context, - } = getAuthContext(extra as AuthenticatedExtra); + } = await getAuthContext(extra as AuthenticatedExtra); // Track server_init on first authenticated request (after client detection) trackServerInit(context); @@ -220,17 +259,36 @@ const handler = createMcpHandler( isError: true, content: [ { - type: 'text' as const, + type: "text" as const, text: `Tool "${tool.name}" is not available in read-only mode`, }, ], }; } + // Check grant-based tool access (runtime filtering for remote server) + const allowedTools = filterToolsForGrant(NEON_TOOLS, grant); + const isAllowed = allowedTools.some((t) => t.name === tool.name); + if (!isAllowed) { + return { + isError: true, + content: [ + { + type: "text" as const, + text: + `Tool "${tool.name}" is not available with the current preset "${grant.preset}"` + + (grant.scopes + ? ` and scopes [${grant.scopes.join(", ")}]` + : ""), + }, + ], + }; + } + const traceId = generateTraceId(); return await startSpan( { - name: 'tool_call', + name: "tool_call", attributes: { tool_name: tool.name, trace_id: traceId, @@ -240,16 +298,18 @@ const handler = createMcpHandler( const properties = { tool_name: tool.name, readOnly: String(readOnly), + preset: grant.preset, + projectScoped: String(!!grant.projectId), clientName: cName, traceId, }; - logger.info('tool call:', properties); + logger.info("tool call:", properties); setSentryTags(context); track({ userId: account.id, - event: 'tool_call', + event: "tool_call", properties, context: { client, @@ -267,25 +327,60 @@ const handler = createMcpHandler( }; try { + // Inject projectId if in project-scoped mode + const effectiveArgs = injectProjectId( + (args ?? {}) as Record, + grant, + ); + + // Enforce protected branch restrictions + enforceProtectedBranches(grant, tool.name, effectiveArgs); + // Wrap args in { params } structure expected by handlers const result = await (toolHandler as any)( - { params: args }, + { params: effectiveArgs }, neonClient, - extraArgs + extraArgs, ); if (result.isError) { - logger.warn('tool error response:', { + logger.warn("tool error response:", { ...properties, isError: true, contentLength: result.content?.length, firstContentType: result.content?.[0]?.type, }); } + + // Append access control warnings to tool response + const accessControlWarnings = getAccessControlWarnings( + grant, + readOnly, + ); + if (accessControlWarnings.length > 0 && result.content) { + result.content.push( + ...accessControlWarnings.map((w: string) => ({ + type: "text" as const, + text: w, + })), + ); + } + return result; } catch (error) { + if (error instanceof GrantViolationError) { + return { + isError: true, + content: [ + { + type: "text" as const, + text: error.message, + }, + ], + }; + } span.setStatus({ code: 2 }); const errorResult = handleToolError(error, properties, traceId); - logger.warn('tool error response:', { + logger.warn("tool error response:", { ...properties, isError: true, contentLength: errorResult.content?.length, @@ -293,61 +388,9 @@ const handler = createMcpHandler( }); return errorResult; } - } + }, ); - } - ); - }); - - // Register all resources - NEON_RESOURCES.forEach((resource) => { - server.registerResource( - resource.name, - resource.uri, - { - description: resource.description, - mimeType: resource.mimeType, }, - async (url: URL, extra: any) => { - const traceId = generateTraceId(); - const properties = { resource_name: resource.name, traceId }; - logger.info('resource call:', properties); - - // Try to get auth context for tracking - let context: ServerContext | undefined; - let account: AuthContext['extra']['account'] | undefined; - let client: AuthContext['extra']['client'] | undefined; - - try { - const authContext = getAuthContext(extra as AuthenticatedExtra); - context = authContext.context; - account = authContext.account; - client = authContext.client; - - // Track server_init on first authenticated request - trackServerInit(context); - - setSentryTags(context); - track({ - userId: account.id, - event: 'resource_call', - properties, - context: { client, app: context.app }, - }); - waitUntil(flushAnalytics()); - } catch { - // Resources can be called without auth in some cases - } - - try { - return await resource.handler(url); - } catch (error) { - captureException(error, { - extra: properties, - }); - throw error; - } - } ); }); @@ -367,19 +410,23 @@ const handler = createMcpHandler( clientName: cName, client, context, - } = getAuthContext(extra as AuthenticatedExtra); + } = await getAuthContext(extra as AuthenticatedExtra); // Track server_init on first authenticated request trackServerInit(context); const traceId = generateTraceId(); - const properties = { prompt_name: prompt.name, clientName: cName, traceId }; - logger.info('prompt call:', properties); + const properties = { + prompt_name: prompt.name, + clientName: cName, + traceId, + }; + logger.info("prompt call:", properties); setSentryTags(context); track({ userId: account.id, - event: 'prompt_call', + event: "prompt_call", properties, context: { client, app: context.app }, }); @@ -395,14 +442,14 @@ const handler = createMcpHandler( const template = await getPromptTemplate( prompt.name, extraArgs, - args + args, ); return { messages: [ { - role: 'user' as const, + role: "user" as const, content: { - type: 'text' as const, + type: "text" as const, text: template, }, }, @@ -414,13 +461,83 @@ const handler = createMcpHandler( }); throw error; } - } + }, ); }); + + // Override tools/list to filter tools based on the authenticated user's grant context. + // By default, the MCP SDK returns ALL registered tools. Since the remote server + // registers all tools upfront (shared across sessions), we need to filter per-request + // so each user only sees tools they can actually use. + server.server.setRequestHandler( + ListToolsRequestSchema, + async (_request, extra) => { + // Access the SDK's internal registered tools (already JSON-schema-converted) + const registeredTools = (server as unknown as Record) + ._registeredTools as Record< + string, + { + enabled: boolean; + title?: string; + description?: string; + inputSchema?: unknown; + outputSchema?: unknown; + annotations?: unknown; + execution?: unknown; + _meta?: unknown; + } + >; + + try { + const { readOnly, grant } = await getAuthContext( + extra as AuthenticatedExtra, + ); + + // Get the names of tools available for this user's grant + read-only context + const availableToolNames: Set = new Set( + getAvailableTools(grant, readOnly).map((t) => t.name), + ); + + // Filter the registered tools (which have already-converted JSON schemas) + const tools = Object.entries(registeredTools) + .filter( + ([name, tool]) => tool.enabled && availableToolNames.has(name), + ) + .map(([name, tool]) => ({ + name, + title: tool.title, + description: tool.description, + inputSchema: tool.inputSchema ?? { type: "object" as const }, + annotations: tool.annotations, + execution: tool.execution, + _meta: tool._meta, + ...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}), + })); + + return { tools }; + } catch { + // If auth fails (e.g., unauthenticated request), fall back to showing all enabled tools + const tools = Object.entries(registeredTools) + .filter(([, tool]) => tool.enabled) + .map(([name, tool]) => ({ + name, + title: tool.title, + description: tool.description, + inputSchema: tool.inputSchema ?? { type: "object" as const }, + annotations: tool.annotations, + execution: tool.execution, + _meta: tool._meta, + ...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}), + })); + + return { tools }; + } + }, + ); }, { serverInfo: { - name: 'mcp-server-neon', + name: "mcp-server-neon", version: pkg.version, }, capabilities: { @@ -433,29 +550,29 @@ const handler = createMcpHandler( }, { redisUrl: process.env.KV_URL || process.env.REDIS_URL, - basePath: '/api', + basePath: "/api", maxDuration: 800, // Fluid Compute - up to 800s for SSE connections - verboseLogs: process.env.NODE_ENV !== 'production', + verboseLogs: process.env.NODE_ENV !== "production", onEvent: (event) => { switch (event.type) { - case 'SESSION_STARTED': - logger.info('MCP session started', { + case "SESSION_STARTED": + logger.info("MCP session started", { sessionId: event.sessionId, transport: event.transport, clientInfo: event.clientInfo, }); break; - case 'SESSION_ENDED': - logger.info('MCP session ended', { + case "SESSION_ENDED": + logger.info("MCP session ended", { sessionId: event.sessionId, transport: event.transport, }); break; - case 'REQUEST_COMPLETED': - if (event.status === 'error') { - logger.warn('MCP request failed', { + case "REQUEST_COMPLETED": + if (event.status === "error") { + logger.warn("MCP request failed", { sessionId: event.sessionId, requestId: event.requestId, method: event.method, @@ -464,21 +581,21 @@ const handler = createMcpHandler( } break; - case 'ERROR': + case "ERROR": const isConnectionError = - typeof event.error === 'string' - ? event.error.includes('No connection established') - : event.error?.message?.includes('No connection established'); + typeof event.error === "string" + ? event.error.includes("No connection established") + : event.error?.message?.includes("No connection established"); if (isConnectionError) { - logger.warn('MCP connection lost', { + logger.warn("MCP connection lost", { sessionId: event.sessionId, source: event.source, severity: event.severity, context: event.context, }); - } else if (event.severity === 'fatal') { - logger.error('MCP fatal error', { + } else if (event.severity === "fatal") { + logger.error("MCP fatal error", { sessionId: event.sessionId, error: event.error, source: event.source, @@ -487,13 +604,13 @@ const handler = createMcpHandler( captureException( event.error instanceof Error ? event.error - : new Error(String(event.error)) + : new Error(String(event.error)), ); } break; } }, - } + }, ); // Cache TTL for API key verification (5 minutes) @@ -502,17 +619,17 @@ const API_KEY_CACHE_TTL_MS = 5 * 60 * 1000; // Helper: Fetch and cache API key details const fetchAccountDetails = async ( - accessToken: string + accessToken: string, ): Promise => { // 1. Check cache first try { const cached = await getApiKeys().get(accessToken); if (cached) { - logger.info('API key cache hit', { accountId: cached.account.id }); + logger.info("API key cache hit", { accountId: cached.account.id }); return cached; } } catch (error) { - logger.warn('API key cache read failed', { error }); + logger.warn("API key cache read failed", { error }); } // 2. Cache miss - verify with Neon API @@ -536,11 +653,11 @@ const fetchAccountDetails = async ( getApiKeys() .set(accessToken, record, API_KEY_CACHE_TTL_MS) .catch((err) => { - logger.warn('API key cache write failed', { err }); - }) + logger.warn("API key cache write failed", { err }); + }), ); - logger.info('API key cache miss, verified and cached', { + logger.info("API key cache miss, verified and cached", { accountId: account.id, }); return record; @@ -549,7 +666,7 @@ const fetchAccountDetails = async ( response?: { status?: number; data?: unknown }; message?: string; }; - logger.error('API key verification failed', { + logger.error("API key verification failed", { message: axiosError.message, status: axiosError.response?.status, data: axiosError.response?.data, @@ -561,15 +678,16 @@ const fetchAccountDetails = async ( // Token verification function with two paths (OAuth tokens + API keys) const verifyToken = async ( req: Request, - bearerToken?: string + bearerToken?: string, ): Promise => { - const userAgent = req.headers.get('user-agent') || undefined; - const readOnlyHeader = req.headers.get('x-read-only'); + const userAgent = req.headers.get("user-agent") || undefined; + const neonReadOnlyHeader = req.headers.get("x-neon-read-only"); + const readOnlyHeader = req.headers.get("x-read-only"); - logger.info('verifyToken called', { + logger.info("verifyToken called", { hasBearerToken: !!bearerToken, bearerTokenLength: bearerToken?.length ?? 0, - tokenPrefix: bearerToken?.substring(0, 10) ?? 'none', + tokenPrefix: bearerToken?.substring(0, 10) ?? "none", userAgent, }); @@ -579,9 +697,13 @@ const verifyToken = async ( // Detect transport from URL pathname const url = new URL(req.url); - const transport: AppContext['transport'] = url.pathname.includes('/mcp') - ? 'stream' - : 'sse'; + const transport: AppContext["transport"] = url.pathname.includes("/mcp") + ? "stream" + : "sse"; + + // Parse grant context from X-Neon-* headers (used only for API key path; + // OAuth tokens use their stored grant from the consent flow instead) + const grant = resolveGrantFromHeaders(req.headers); // ============================================ // PATH 1: Check OAuth tokens table FIRST @@ -593,12 +715,18 @@ const verifyToken = async ( // Expiration is checked by withMcpAuth using expiresAt field // which returns proper RFC-compliant 401 with WWW-Authenticate header - logger.info('OAuth token found', { clientId: token.client.id }); + logger.info("OAuth token found", { clientId: token.client.id }); + + // OAuth tokens always use the stored grant from the consent flow. + // Headers are fully ignored — grant is immutable until re-authentication. + const effectiveGrant = resolveGrantFromToken( + token as { grant?: GrantContext }, + ); - // Determine read-only mode from header and OAuth scope + // Read-only is determined only by stored OAuth scope and grant preset (no headers) const readOnly = isReadOnly({ - headerValue: readOnlyHeader, scope: token.scope, + grant: effectiveGrant, }); // Return auth from stored token (0 API calls!) @@ -606,7 +734,7 @@ const verifyToken = async ( token: token.accessToken, scopes: Array.isArray(token.scope) ? token.scope - : token.scope?.split(' ') ?? ['read', 'write'], + : (token.scope?.split(" ") ?? ["read", "write"]), clientId: token.client.id, expiresAt: token.expires_at ? Math.floor(token.expires_at / 1000) @@ -620,6 +748,7 @@ const verifyToken = async ( }, apiKey: bearerToken, readOnly, + grant: effectiveGrant, client: { id: token.client.id, name: token.client.client_name, @@ -630,14 +759,14 @@ const verifyToken = async ( }; } } catch (error) { - logger.warn('OAuth token lookup failed, trying API key path', { error }); + logger.warn("OAuth token lookup failed, trying API key path", { error }); } // ============================================ // PATH 2: Not an OAuth token - try API key // (For direct API key usage) // ============================================ - logger.info('Trying API key verification path', { + logger.info("Trying API key verification path", { tokenPrefix: bearerToken.substring(0, 10), }); @@ -646,19 +775,22 @@ const verifyToken = async ( return undefined; } - // Determine read-only mode from header only (API keys don't have OAuth scopes) + // Determine read-only mode from header and grant preset const readOnly = isReadOnly({ + neonHeaderValue: neonReadOnlyHeader, headerValue: readOnlyHeader, + grant, }); return { token: bearerToken, - scopes: ['*'], // API keys get all scopes - clientId: 'api-key', // Literal string + scopes: ["*"], // API keys get all scopes + clientId: "api-key", // Literal string extra: { account: apiKeyRecord.account, apiKey: bearerToken, readOnly, + grant, transport, userAgent, }, @@ -668,7 +800,7 @@ const verifyToken = async ( // Wrap with authentication const authHandler = withMcpAuth(handler, verifyToken, { required: true, - resourceMetadataPath: '/.well-known/oauth-protected-resource', + resourceMetadataPath: "/.well-known/oauth-protected-resource", }); // Normalize legacy paths (/mcp, /sse) to canonical /api/* paths @@ -681,10 +813,10 @@ const authHandler = withMcpAuth(handler, verifyToken, { const handleRequest = (req: Request) => { const url = new URL(req.url); - if (url.pathname === '/mcp') { - url.pathname = '/api/mcp'; - } else if (url.pathname === '/sse') { - url.pathname = '/api/sse'; + if (url.pathname === "/mcp") { + url.pathname = "/api/mcp"; + } else if (url.pathname === "/sse") { + url.pathname = "/api/sse"; } const normalizedReq = new Request(url.toString(), { @@ -692,7 +824,7 @@ const handleRequest = (req: Request) => { headers: req.headers, body: req.body, // @ts-expect-error duplex is required for streaming bodies - duplex: 'half', + duplex: "half", }); return authHandler(normalizedReq); diff --git a/landing/app/api/authorize/route.ts b/landing/app/api/authorize/route.ts index 59241ebc..a6be01ea 100644 --- a/landing/app/api/authorize/route.ts +++ b/landing/app/api/authorize/route.ts @@ -1,20 +1,55 @@ -import { NextRequest, NextResponse } from 'next/server'; -import he from 'he'; -import { model } from '../../../mcp-src/oauth/model'; -import { upstreamAuth } from '../../../lib/oauth/client'; +import { NextRequest, NextResponse } from "next/server"; +import he from "he"; +import { model } from "../../../mcp-src/oauth/model"; +import { upstreamAuth } from "../../../lib/oauth/client"; import { isClientAlreadyApproved, updateApprovedClientsCookie, -} from '../../../lib/oauth/cookies'; -import { COOKIE_SECRET } from '../../../lib/config'; -import { handleOAuthError } from '../../../lib/errors'; +} from "../../../lib/oauth/cookies"; +import { COOKIE_SECRET } from "../../../lib/config"; +import { handleOAuthError } from "../../../lib/errors"; import { hasWriteScope, SCOPE_DEFINITIONS, SUPPORTED_SCOPES, -} from '../../../mcp-src/utils/read-only'; -import { logger } from '../../../mcp-src/utils/logger'; -import { matchesRedirectUri } from '../../../lib/oauth/redirect-uri'; +} from "../../../mcp-src/utils/read-only"; +import { logger } from "../../../mcp-src/utils/logger"; +import { matchesRedirectUri } from "../../../lib/oauth/redirect-uri"; +import { + PRESETS, + PRESET_DEFINITIONS, + SCOPE_CATEGORIES, + SCOPE_CATEGORY_DEFINITIONS, + type GrantContext, + type Preset, + type ScopeCategory, +} from "../../../mcp-src/utils/grant-context"; +import { NEON_TOOLS } from "../../../mcp-src/tools/definitions"; + +/** + * Generates minimal tool data for client-side filtering and display. + */ +function getToolDataJson(): string { + const data = NEON_TOOLS.map((tool) => ({ + name: tool.name, + title: tool.annotations?.title ?? tool.name, + scope: tool.scope, + readOnly: tool.readOnlySafe, + destructive: !!tool.annotations?.destructiveHint, + })); + return JSON.stringify(data); +} + +/** + * Generates scope category labels for client-side display. + */ +function getScopeLabelsJson(): string { + const labels: Record = {}; + for (const cat of SCOPE_CATEGORIES) { + labels[cat] = SCOPE_CATEGORY_DEFINITIONS[cat].label; + } + return JSON.stringify(labels); +} export type DownstreamAuthRequest = { responseType: string; @@ -24,25 +59,26 @@ export type DownstreamAuthRequest = { state: string; codeChallenge?: string; codeChallengeMethod?: string; + grant?: GrantContext; }; const parseAuthRequest = ( searchParams: URLSearchParams, ): DownstreamAuthRequest => { - const responseType = searchParams.get('response_type') || ''; - const clientId = searchParams.get('client_id') || ''; - const redirectUri = searchParams.get('redirect_uri') || ''; - const scope = searchParams.get('scope') || ''; - const state = searchParams.get('state') || ''; - const codeChallenge = searchParams.get('code_challenge') || undefined; + const responseType = searchParams.get("response_type") || ""; + const clientId = searchParams.get("client_id") || ""; + const redirectUri = searchParams.get("redirect_uri") || ""; + const scope = searchParams.get("scope") || ""; + const state = searchParams.get("state") || ""; + const codeChallenge = searchParams.get("code_challenge") || undefined; const codeChallengeMethod = - searchParams.get('code_challenge_method') || 'plain'; + searchParams.get("code_challenge_method") || "plain"; return { responseType, clientId, redirectUri, - scope: scope.split(' ').filter(Boolean), + scope: scope.split(" ").filter(Boolean), state, codeChallenge, codeChallengeMethod, @@ -50,19 +86,185 @@ const parseAuthRequest = ( }; /** - * Renders the scope selection UI. - * Read access is always granted. Write access is always shown as an option. + * Renders scope category checkboxes for the custom preset. + */ +function renderScopeCategoryCheckboxes(): string { + let html = ""; + for (const category of SCOPE_CATEGORIES) { + const def = SCOPE_CATEGORY_DEFINITIONS[category]; + const sensitiveHtml = def.sensitive + ? ' SENSITIVE' + : ""; + html += ` + + `; + } + return html; +} + +/** + * Renders scope categories as read-only indicators for the permission details view. + */ +function renderPermissionDetailItems(): string { + let html = ""; + for (const category of SCOPE_CATEGORIES) { + const def = SCOPE_CATEGORY_DEFINITIONS[category]; + const sensitiveHtml = def.sensitive + ? ' SENSITIVE' + : ""; + html += ` +
+ \u2713 +
+ ${he.escape(def.label)}${sensitiveHtml} + ${he.escape(def.description)} +
+
+ `; + } + return html; +} + +/** + * Renders the preset selection UI with tab-style buttons, scope category + * checkboxes for custom preset, collapsible permission details, branch + * protection, and caution banner. + */ +function renderPresetSection(projectId: string | null): string { + // Hidden inputs for OAuth scopes (updated by JS based on preset) + let html = ``; + html += ``; + html += ``; + + // Preset tab group + html += `
Presets
`; + html += `
`; + const presetOrder: Preset[] = [ + "custom", + "local_development", + "production_use", + "full_access", + ]; + for (const preset of presetOrder) { + const def = PRESET_DEFINITIONS[preset]; + const isDefault = preset === "full_access"; + html += ` + + `; + } + html += `
`; + + // Preset description (updated dynamically by JS) + html += `

${he.escape(PRESET_DEFINITIONS.full_access.description)}

`; + + // Permission details (collapsible, shown for non-custom presets) + html += ` +
+ Show permission details +
+ ${renderPermissionDetailItems()} +
+
+ `; + + // Custom scope categories (hidden by default, shown when custom is selected) + html += ` + + `; + + // Project scope + const projectIdValue = projectId ? he.escape(projectId) : ""; + const projectIdReadonly = projectId ? " readonly" : ""; + html += ` +
+
+ Project scope + Restrict access to a single Neon project (optional) +
+ +
+ `; + + // Protect production branches + html += ` +
+ +
+ `; + + // Tools preview (populated by client-side JS) + html += ` +
+
+ Included tools + +
+
+
+ `; + + // Caution banner + html += ` +
+ Caution: AI agents can make mistakes. We recommend avoiding SQL execution on production databases. + Use branch protection and consider read-only access for sensitive environments. +
+ `; + + return html; +} + +/** + * Renders the scope selection as read-only display (when preset is pre-configured). */ function renderScopeSection(requestedScopes: string[]): string { const writeChecked = requestedScopes.length === 0 || hasWriteScope(requestedScopes); - // Read access is always granted (hidden input ensures it's submitted) let html = ``; html += `
- + \u2713
${he.escape(SCOPE_DEFINITIONS.read.label)} ${he.escape(SCOPE_DEFINITIONS.read.description)} @@ -76,7 +278,7 @@ function renderScopeSection(requestedScopes: string[]): string { type="checkbox" name="scopes" value="write" - ${writeChecked ? 'checked' : ''} + ${writeChecked ? "checked" : ""} class="scope-checkbox" />
@@ -99,8 +301,10 @@ const renderApprovalDialog = ( }, state: string, requestedScopes: string[], + projectId: string | null = null, + showPresets: boolean = true, ) => { - const clientName = he.escape(client.client_name || 'A new MCP Client'); + const clientName = he.escape(client.client_name || "A new MCP Client"); const website = client.client_uri ? he.escape(client.client_uri) : undefined; const redirectUris = client.redirect_uris; @@ -112,7 +316,7 @@ const renderApprovalDialog = ( ${website}
` - : ''; + : ""; const redirectUrisHtml = redirectUris && redirectUris.length > 0 @@ -120,10 +324,10 @@ const renderApprovalDialog = (
Redirect URIs:
- ${redirectUris.map((uri) => `
${he.escape(uri)}
`).join('')} + ${redirectUris.map((uri) => `
${he.escape(uri)}
`).join("")}
` - : ''; + : ""; const html = ` @@ -134,16 +338,22 @@ const renderApprovalDialog = ( ${clientName} | Authorization Request