1# neon mcp guide for plyr.fm 2 3the [neon mcp server](https://github.com/neondatabase/mcp-server-neon) provides tools for interacting with neon postgres databases through the model context protocol. this guide covers read-only operations useful for inspecting and debugging plyr.fm's database. 4 5## overview 6 7the neon mcp is integrated into claude code and provides direct access to: 8- project and branch management 9- database schema inspection 10- SQL query execution 11- connection string generation 12 13this guide focuses on **read-only operations** - querying and inspecting data without modifications. 14 15## discovering your projects 16 17### list all projects 18 19```json 20mcp__neon__list_projects({}) 21``` 22 23returns all neon projects in your account with details like: 24- project name and ID 25- region (aws-us-east-1, aws-us-east-2, etc.) 26- postgres version 27- compute usage stats 28- storage size 29 30**plyr.fm projects:** 31- `plyr-prd` (cold-butterfly-11920742) - production (us-east-1) 32- `plyr-stg` (frosty-math-37367092) - staging (us-west-2) 33- `plyr-dev` (muddy-flower-98795112) - development (us-east-2) 34 35### get project details 36 37```json 38mcp__neon__describe_project({ 39 "projectId": "muddy-flower-98795112" 40}) 41``` 42 43shows branches, compute endpoints, and configuration for a specific project. 44 45## exploring database structure 46 47### list all tables 48 49```json 50mcp__neon__get_database_tables({ 51 "projectId": "muddy-flower-98795112" 52}) 53``` 54 55**plyr.fm tables:** 56- `tracks` - uploaded music files with metadata 57- `artists` - user profiles (DIDs, handles, display names) 58- `track_likes` - like interactions between users and tracks 59- `user_sessions` - oauth session management 60- `oauth_states` - oauth flow state tracking 61- `exchange_tokens` - token exchange for auth 62- `user_preferences` - user settings 63- `queue_state` - processing queue status 64- `alembic_version` - database migration tracking 65 66### inspect table schema 67 68```json 69mcp__neon__describe_table_schema({ 70 "projectId": "muddy-flower-98795112", 71 "tableName": "tracks" 72}) 73``` 74 75returns detailed schema information: 76- column names, types, nullable, defaults 77- indexes (btree, unique, etc.) 78- constraints (primary keys, foreign keys) 79- table sizes 80 81**key tracks columns:** 82- `id` (integer, primary key) 83- `title` (varchar, not null) 84- `file_id` (varchar, not null) - R2 storage key 85- `file_type` (varchar, not null) - audio format 86- `artist_did` (varchar, foreign key → artists) 87- `atproto_record_uri` (varchar, nullable) - ATProto record reference 88- `atproto_record_cid` (varchar, nullable) - content identifier 89- `play_count` (integer, default 0) 90- `features` (jsonb, default []) - collaborating artists 91- `image_id` (varchar, nullable) - album art reference 92- `extra` (jsonb, default {}) - additional metadata (album, etc.) 93 94**key artists columns:** 95- `did` (varchar, primary key) - ATProto decentralized identifier 96- `handle` (varchar, not null) - user handle 97- `display_name` (varchar, not null) 98- `pds_url` (varchar, nullable) - custom PDS endpoint 99 100### visualize database structure 101 102```json 103mcp__neon__describe_branch({ 104 "projectId": "muddy-flower-98795112", 105 "branchId": "br-crimson-recipe-aesyo0p9" 106}) 107``` 108 109returns a tree view showing all databases, schemas, tables, indexes, functions, and sequences. 110 111## running queries 112 113### basic query execution 114 115```json 116mcp__neon__run_sql({ 117 "projectId": "muddy-flower-98795112", 118 "sql": "SELECT COUNT(*) FROM tracks" 119}) 120``` 121 122**important notes:** 123- always provide `projectId` 124- queries run on the default branch unless `branchId` is specified 125- uses default database `neondb` unless `databaseName` is specified 126- results returned as JSON array of objects 127 128### useful plyr.fm queries 129 130#### overview stats 131 132```sql 133-- total tracks and artists 134SELECT 135 COUNT(*) as total_tracks, 136 COUNT(DISTINCT artist_did) as total_artists 137FROM tracks; 138 139-- atproto integration status 140SELECT 141 COUNT(*) FILTER (WHERE atproto_record_uri IS NOT NULL) as synced_tracks, 142 COUNT(*) FILTER (WHERE atproto_record_uri IS NULL) as unsynced_tracks, 143 COUNT(*) FILTER (WHERE image_id IS NOT NULL) as tracks_with_images, 144 COUNT(*) FILTER (WHERE image_id IS NULL) as tracks_without_images 145FROM tracks; 146 147-- engagement metrics 148SELECT 149 COUNT(*) as total_likes, 150 COUNT(DISTINCT user_did) as unique_likers, 151 COUNT(DISTINCT track_id) as liked_tracks 152FROM track_likes; 153 154-- storage stats by file type 155SELECT 156 file_type, 157 COUNT(*) as count, 158 COUNT(*) FILTER (WHERE image_id IS NOT NULL) as with_artwork 159FROM tracks 160GROUP BY file_type 161ORDER BY count DESC; 162``` 163 164#### artist analytics 165 166```sql 167-- artist leaderboard by uploads 168SELECT 169 a.handle, 170 a.display_name, 171 COUNT(t.id) as track_count, 172 SUM(t.play_count) as total_plays, 173 a.pds_url 174FROM artists a 175LEFT JOIN tracks t ON a.did = t.artist_did 176GROUP BY a.did, a.handle, a.display_name, a.pds_url 177ORDER BY track_count DESC, total_plays DESC; 178 179-- tracks per artist 180SELECT 181 artist_did, 182 COUNT(*) as track_count 183FROM tracks 184GROUP BY artist_did 185ORDER BY track_count DESC; 186``` 187 188#### track discovery 189 190```sql 191-- recent uploads 192SELECT 193 t.id, 194 t.title, 195 a.handle, 196 a.display_name, 197 t.play_count, 198 t.created_at, 199 t.atproto_record_uri IS NOT NULL as has_atproto_record, 200 t.image_id IS NOT NULL as has_image 201FROM tracks t 202JOIN artists a ON t.artist_did = a.did 203ORDER BY t.created_at DESC 204LIMIT 10; 205 206-- most popular tracks 207SELECT 208 t.id, 209 t.title, 210 a.handle, 211 t.play_count, 212 COUNT(tl.id) as like_count 213FROM tracks t 214JOIN artists a ON t.artist_did = a.did 215LEFT JOIN track_likes tl ON t.id = tl.track_id 216GROUP BY t.id, t.title, a.handle, t.play_count 217ORDER BY t.play_count DESC, like_count DESC 218LIMIT 10; 219 220-- tracks with album metadata 221SELECT 222 t.title, 223 t.extra->>'album' as album, 224 a.handle 225FROM tracks t 226JOIN artists a ON t.artist_did = a.did 227WHERE t.extra->>'album' IS NOT NULL; 228``` 229 230#### time-series analysis 231 232```sql 233-- uploads per day 234SELECT 235 DATE_TRUNC('day', created_at) as day, 236 COUNT(*) as uploads 237FROM tracks 238GROUP BY day 239ORDER BY day DESC; 240 241-- engagement trends 242SELECT 243 DATE_TRUNC('day', tl.created_at) as day, 244 COUNT(*) as likes_given 245FROM track_likes tl 246GROUP BY day 247ORDER BY day DESC; 248``` 249 250#### atproto integration debugging 251 252```sql 253-- tracks missing atproto records 254SELECT 255 t.id, 256 t.title, 257 t.artist_did, 258 a.handle, 259 a.pds_url, 260 t.created_at 261FROM tracks t 262JOIN artists a ON t.artist_did = a.did 263WHERE t.atproto_record_uri IS NULL 264ORDER BY t.created_at DESC 265LIMIT 20; 266 267-- verify atproto record URIs format 268SELECT 269 id, 270 title, 271 atproto_record_uri, 272 atproto_record_cid, 273 created_at 274FROM tracks 275WHERE atproto_record_uri IS NOT NULL 276ORDER BY created_at DESC 277LIMIT 10; 278 279-- check for uri/cid mismatches (uri present but cid missing or vice versa) 280SELECT 281 id, 282 title, 283 atproto_record_uri IS NOT NULL as has_uri, 284 atproto_record_cid IS NOT NULL as has_cid 285FROM tracks 286WHERE (atproto_record_uri IS NULL) != (atproto_record_cid IS NULL) 287LIMIT 20; 288``` 289 290#### jsonb field queries 291 292```sql 293-- query features array (collaborations) 294SELECT 295 t.title, 296 a.handle, 297 jsonb_array_length(t.features) as feature_count, 298 t.features 299FROM tracks t 300JOIN artists a ON t.artist_did = a.did 301WHERE jsonb_array_length(t.features) > 0; 302 303-- query extra metadata 304SELECT 305 title, 306 extra->>'album' as album, 307 extra->>'genre' as genre, 308 extra 309FROM tracks 310WHERE extra != '{}'::jsonb; 311``` 312 313## connection management 314 315### get connection string 316 317```json 318mcp__neon__get_connection_string({ 319 "projectId": "muddy-flower-98795112" 320}) 321``` 322 323returns a postgres connection string for use with `psql`, database clients, or application configs. 324 325**optional parameters:** 326- `branchId` - specific branch (defaults to main) 327- `databaseName` - specific database (defaults to neondb) 328- `roleName` - specific role (defaults to neondb_owner) 329 330**example output:** 331``` 332postgresql://neondb_owner:npg_6CNUVfgtz8bY@ep-flat-haze-aefjvcba-pooler.c-2.us-east-2.aws.neon.tech/neondb?channel_binding=require&sslmode=require 333``` 334 335## database environment mapping 336 337plyr.fm uses different neon projects for each environment: 338 339| environment | project name | project ID | region | endpoint | 340|------------|--------------|-----------|---------|----------| 341| dev | plyr-dev | muddy-flower-98795112 | us-east-2 | ep-flat-haze-aefjvcba | 342| staging | plyr-staging | frosty-math-37367092 | us-west-2 | (varies) | 343| prod | plyr | cold-butterfly-11920742 | us-east-1 | ep-young-poetry-a4ueyq14 | 344 345**in .env:** 346- default `DATABASE_URL` points to dev (plyr-dev) 347- prod connection string is commented out 348- admin scripts use `ADMIN_DATABASE_URL` for prod operations 349 350## common workflows 351 352### debugging orphaned records 353 354```sql 355-- 1. check for tracks without atproto records 356SELECT COUNT(*) FROM tracks WHERE atproto_record_uri IS NULL; 357 358-- 2. identify which artists have orphaned tracks 359SELECT 360 a.handle, 361 COUNT(*) as orphaned_tracks 362FROM tracks t 363JOIN artists a ON t.artist_did = a.did 364WHERE t.atproto_record_uri IS NULL 365GROUP BY a.handle; 366 367-- 3. compare with pdsx output 368-- use pdsx to list records on PDS: 369-- uvx pdsx --pds https://pds.zzstoatzz.io -r zzstoatzz.io ls fm.plyr.track 370``` 371 372### verifying backfill success 373 374after running `scripts/backfill_atproto_records.py`: 375 376```sql 377-- 1. count records with ATProto URIs 378SELECT 379 COUNT(*) FILTER (WHERE atproto_record_uri IS NOT NULL) as synced, 380 COUNT(*) FILTER (WHERE atproto_record_uri IS NULL) as unsynced 381FROM tracks; 382 383-- 2. check specific tracks mentioned in backfill 384SELECT 385 title, 386 atproto_record_uri, 387 atproto_record_cid 388FROM tracks 389WHERE title IN ('webhook', 'maxwell', 'ccr') 390ORDER BY created_at DESC; 391 392-- 3. verify image_id populated for tracks with artwork 393SELECT 394 title, 395 image_id IS NOT NULL as has_image, 396 atproto_record_uri 397FROM tracks 398WHERE title = 'ccr'; 399``` 400 401### investigating performance issues 402 403```sql 404-- find most active artists (could cause rate limiting) 405SELECT 406 a.handle, 407 COUNT(t.id) as track_count, 408 MAX(t.created_at) as last_upload, 409 a.pds_url 410FROM artists a 411JOIN tracks t ON a.did = t.artist_did 412GROUP BY a.handle, a.pds_url 413ORDER BY track_count DESC; 414 415-- check upload patterns for rate limit issues 416SELECT 417 DATE_TRUNC('hour', created_at) as hour, 418 COUNT(*) as uploads 419FROM tracks 420WHERE created_at > NOW() - INTERVAL '24 hours' 421GROUP BY hour 422ORDER BY hour DESC; 423``` 424 425### schema evolution tracking 426 427```sql 428-- check current migration version 429SELECT version_num FROM alembic_version; 430 431-- inspect recent schema changes via table sizes 432SELECT 433 table_name, 434 pg_size_pretty(pg_total_relation_size(quote_ident(table_name)::regclass)) as total_size 435FROM information_schema.tables 436WHERE table_schema = 'public' 437 AND table_type = 'BASE TABLE' 438ORDER BY pg_total_relation_size(quote_ident(table_name)::regclass) DESC; 439``` 440 441## tips and best practices 442 443### jsonb field access 444 445use `->` for json objects and `->>` for text extraction: 446 447```sql 448-- returns json object 449SELECT extra->'metadata' FROM tracks; 450 451-- returns text value 452SELECT extra->>'album' FROM tracks; 453 454-- filter by nested json 455SELECT * FROM tracks WHERE (extra->>'album')::text = 'covid ableton sessions'; 456``` 457 458### counting with filters 459 460postgres supports conditional aggregates: 461 462```sql 463SELECT 464 COUNT(*) as total, 465 COUNT(*) FILTER (WHERE play_count > 0) as played, 466 COUNT(*) FILTER (WHERE play_count = 0) as unplayed 467FROM tracks; 468``` 469 470### working with timestamps 471 472```sql 473-- last 24 hours 474WHERE created_at > NOW() - INTERVAL '24 hours' 475 476-- specific date range 477WHERE created_at BETWEEN '2025-11-01' AND '2025-11-12' 478 479-- group by day/week/month 480DATE_TRUNC('day', created_at) 481DATE_TRUNC('week', created_at) 482DATE_TRUNC('month', created_at) 483``` 484 485### project id shortcuts 486 487store frequently used project IDs in your notes: 488 489``` 490dev: muddy-flower-98795112 491staging: frosty-math-37367092 492prod: cold-butterfly-11920742 493``` 494 495## limitations 496 4971. **read-only focus**: this guide covers read operations only. for migrations, use alembic or neon's schema migration tools. 498 4992. **no write operations**: the neon mcp supports writes, but they're not covered here to prevent accidental data modifications during debugging. 500 5013. **connection pooling**: queries go through connection poolers (`-pooler` in endpoint). for admin operations or schema changes, use direct endpoints. 502 5034. **query timeouts**: complex queries may timeout. break them into smaller operations or add indexes if slow. 504 5055. **default database**: most operations assume `neondb` database. specify `databaseName` if using different database. 506 507## related tools 508 509- **pdsx**: for inspecting ATProto records on PDS (see docs/tools/pdsx.md) 510- **psql**: for interactive postgres sessions using connection strings 511- **alembic**: for database migrations (see alembic/versions/) 512- **neon console**: web UI at https://console.neon.tech 513 514## references 515 516- neon mcp server: https://github.com/neondatabase/mcp-server-neon 517- plyr.fm database models: src/backend/models/ 518- ATProto integration: src/backend/_internal/atproto/records.py 519- migration scripts: scripts/backfill_atproto_records.py