# AGENTS.md — naco-api ## Quick commands ```bash # Verify (no CI — match these locally) gofmt -l . # fail if any output go build -v ./... go test -v -cover ./... # Single package / single test go test -v -cover ./internal/server/... -run TestGetPost go test -v -cover ./internal/bluesky/... # Integration tests (requires Postgres + pgvector; never use prod DB) docker compose -f docker-compose.test.yml up -d # postgres:5432 + ollama:11434 TEST_DATABASE_URL="postgres://postgres:postgres@localhost:5432/mcpserver?sslmode=disable" \ go test -v -cover ./internal/store/... ``` ## What this is Go MCP server (13 tools) for **Bluesky / Nostr / ActivityPub / Twitter**. Every tool is gated by **x402** (HTTP 402 crypto payments, EVM `base`); settlement → Postgres, optional **Matrix** webhook. Tools: `getPost`, `getLatestPosts`, `getPostsInTime`, `getFeed` (free), `getActor`, `searchActors`, `getStyleProfile`, `findSimilarUsers`, `getEngagementMetrics`, `getPostingPatterns`, `getTopicModeling`, `getSentimentTrends`, `getEngagementDrivers`. ## Architecture ``` cmd/server/main.go entrypoint — wires config, DB, all clients, readyz sidecar internal/server/ MCP handlers + x402 + analytics (~1800 lines in server.go — start there) server.go tool dispatch, settlement, prewarm, cache analytics.go analytics compute + coverage/cache logic backends.go platform/backend interfaces (fakes for tests) storefake_test.go in-memory Store fake (DB-free analytics tests) types.go request/response types, platform enum auth.go / webhook.go / cache*.go / *Middleware.go / networkInfo.go *_test.go split suites: mcp_test.go, analytics_test.go, validation_test.go, etc. internal/activitypub/ AP client; throttle.go per-host token bucket (1/s) for background GTS warming internal/{bluesky,nostr,twitter,ollama,config,utils,actor,x402} sql/schema/ 001..007 goose migrations sql/queries/ sqlc query files → internal/store/ config/models.env single source of truth for EMBEDDING_MODEL / STYLE_MODEL testClient/rest/ manual live REST endpoint client (not part of go test) testClient/mcp/ manual live MCP endpoint client (not part of go test) ``` ## Code generation `sqlc` generates `internal/store/*.go` from `sql/queries/*.sql` + `sql/schema/`: ```bash sqlc generate # after editing any file in sql/ ``` Never hand-edit `// Code generated by sqlc. DO NOT EDIT.` files. ## Migrations Goose-style (`-- +goose up` / `-- +goose down`) in `sql/schema/` — 7 files `001_transactions.sql` … `007_add_analytics_coverage.sql`. Applied at boot when `MIGRATE_ON_START=true` (default `true`). New files must be numbered sequentially (`008_*.sql`). ## Testing patterns - Interface fakes in `internal/server/backends.go` + `storefake_test.go` — no live clients/DB needed. - Call tools via `callTool(s, name, args)` through the MCP handler (see `mcp_test.go:302`). - Integration `internal/store/integration_test.go` skipped unless `TEST_DATABASE_URL` set — use `docker-compose.test.yml` (pgvector:pg16). Point it at a throwaway DB. - `internal/server` tests split by concern (`analytics_test.go`, `searchactors_test.go`, `styleprofile_test.go`, `validation_test.go`, `coverage_test.go`, `twitter_analytics_test.go`); `mcp_test.go` holds cross-cutting dispatch/cache. ## Config Loaded in order: `config/models.env` → `.env` (gitignored, via `godotenv`) → env. `getEnv(k, fallback)` fatals if fallback `""` and var unset. | Variable | Default | Notes | |---|---|---| | `GOOSE_DBSTRING` | `postgres:///mcpserver` | Server DB (not tests) | | `TEST_DATABASE_URL` | — | **Tests only**, separate DB | | `SERVER_ADDRESS` / `SERVER_PORT` | `127.0.0.1` / `8080` | Unix socket via x402 `X402Server`; TCP sidecar on `READYZ_PORT` | | `READYZ_PORT` | `9090` | Sidecar serves `/readyz` (DB+Ollama health) + `/metrics` | | `MIGRATE_ON_START` | `true` | Apply `sql/schema` on boot | | `OLLAMA_HOST` | `http://localhost:11434` | Models from `config/models.env` | | `MATRIX_WEBHOOK_URL` | — | If set, payment notifications | | `X402_FACILITATOR_URL` | `https://facilitator.x402.rs` | | | `ACTIVE_NETWORKS` | `base` | Comma eip155 IDs | | `ENRICH_CROSS_PLATFORM` | `true` | | `findProjectRoot()` walks up to `go.mod` to locate `config/models.env`; tests with `os.Chdir` can break it. ## Gotchas - `bin/` gitignored — `go build -o bin/mcpserver ./cmd/server/`. - Server DB `GOOSE_DBSTRING` vs test DB `TEST_DATABASE_URL` intentionally separate. - `testClient/` hits live `api.naco.li` — not run by `go test`. - `server.go` is large; new tool: register in `registerTools()` and add `func (s *Server) myTool(*mcp.CallToolRequest)`. - Unix socket MCP + TCP readyz sidecar; `mcpServer.service` expects `bin/main` at repo root. - Platform inferred from author handle in `determinePlatform`/`fetch*` — not caller-supplied. - Bluesky auth refresh via `isAuthError`/`withRefresh` — 401/expired token retried once. - Twitter client clamps `limit` (`>100` → 100), retries 429/5xx/transport errors with `x-rate-limit-*`/`Retry-After` backoff (bounded by `maxRateLimitWait`), and has a 60s per-request timeout. - ActivityPub client: GTS Mastodon API only serves locally-federated statuses. Remote actors are resolved via `/accounts/search?resolve=true` (not `/accounts/lookup`, which skips WebFinger). When the local cache can't satisfy full `limit` (clamped to 200), `GetLatestNPosts`/`GetPostsInTimeframe` fall back to walking the remote outbox directly (webfinger → actor → `OrderedCollection`/`OrderedCollectionPage`), filtering `Create`/`Note`-like activities and dereferencing IRI-only objects; `GetPost` derefs status URLs over AP when GTS `/search` can't resolve them. The `read:search` scope is needed for status-URL resolution. Outbox-fetched posts are warmed into GTS in background via `GET /api/v2/search?resolve=true&type=statuses&limit=1` per post, throttled at 1 req/s per remote host via per-host token bucket (`internal/activitypub/throttle.go`) with cross-host concurrency, no follows. - Coverage ranges for analytics are interval-merged; disjoint gaps must remain uncovered (see `coverage_test.go`).