{"id":50246567,"url":"https://github.com/noblepayne/pinboard-mcp","last_synced_at":"2026-05-26T23:16:16.917Z","repository":{"id":344483953,"uuid":"1181983426","full_name":"noblepayne/pinboard-mcp","owner":"noblepayne","description":"Babashka Streamable HTTP MCP server wrapping the Pinboard bookmarking API. AI agents can list, search, add, and delete bookmarks through MCP tools. Compatible with mcp-injector.","archived":false,"fork":false,"pushed_at":"2026-03-15T08:23:37.000Z","size":63,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-15T08:33:46.559Z","etag":null,"topics":["agents","ai","ai-agents","babashka","clojure","llm","mcp","mcp-server","mcp-servers","pinboard"],"latest_commit_sha":null,"homepage":"https://noblepayne.com","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/noblepayne.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-03-14T22:13:06.000Z","updated_at":"2026-03-15T08:24:22.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/noblepayne/pinboard-mcp","commit_stats":null,"previous_names":["noblepayne/pinboard-mcp"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/noblepayne/pinboard-mcp","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/noblepayne%2Fpinboard-mcp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/noblepayne%2Fpinboard-mcp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/noblepayne%2Fpinboard-mcp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/noblepayne%2Fpinboard-mcp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/noblepayne","download_url":"https://codeload.github.com/noblepayne/pinboard-mcp/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/noblepayne%2Fpinboard-mcp/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33542894,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"ssl_error","status_checked_at":"2026-05-26T15:22:15.568Z","response_time":63,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agents","ai","ai-agents","babashka","clojure","llm","mcp","mcp-server","mcp-servers","pinboard"],"created_at":"2026-05-26T23:16:14.603Z","updated_at":"2026-05-26T23:16:16.912Z","avatar_url":"https://github.com/noblepayne.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# pinboard-mcp\n\nA Babashka Streamable HTTP MCP server wrapping the Pinboard bookmarking API. AI agents can manage bookmarks better than humans typing commands.\n\n**Transport:** MCP Streamable HTTP (spec `2025-03-26`)\n**Endpoint:** Single `/mcp` POST + `/health`\n**Compatible with:** mcp-injector `{:pinboard {:url \"http://127.0.0.1:PORT/mcp\"}}`\n\n## Project Structure\n\n```\npinboard-mcp/\n├── pinboard_mcp.bb    # Single-file MCP server (the whole thing)\n├── bb.edn             # Tasks: run, start, test, lint, health\n├── flake.nix          # Nix package + NixOS service module\n├── README.md          # This file\n├── AGENTS.md          # Agent-specific context\n├── SPEC.md            # Original specification\n├── DEVLOG.md          # Development notes\n└── tests/\n    └── test_pinboard_mcp.clj  # Integration tests (fake API)\n```\n\n## Workflow\n\n- **Test-driven** — tests guide development, write tests that verify real client usage\n- **Integration tests only** — fake API server, no mocks, test like a client would\n- **Clean lint** — no warnings tolerated (`clj-kondo`)\n- **Formatting** — uniform across all types:\n  - Clojure/Babashka: `nix run nixpkgs#cljfmt -- fix \u003cfile\u003e`\n  - Markdown: `nix run nixpkgs#mdformat -- \u003cfile\u003e`\n  - Nix: `nix fmt .`\n  - EDN: `clojure.pprint`\n- **Feature branches** — commit often as snapshots, rewrite history later\n- **Docs up to date** — update before commit\n- **Keep bb.edn current** — tasks mirror actual commands\n\n## Running\n\n```bash\n# Dev — OS-assigned port, logs JSON startup line\nbb run\n\n# Dev — fixed port 3030 (or PINBOARD_MCP_PORT)\nbb start\n\n# Health check\nbb health\n```\n\nAuth via env vars or `~/.config/pinboard/config.edn`:\n\n```bash\nexport PINBOARD_TOKEN=\"username:abcd1234\"\n```\n\n```clojure\n;; ~/.config/pinboard/config.edn\n{:token \"username:abcd1234\"}\n```\n\n## mcp-injector Config\n\nAdd to `mcp-servers.edn`:\n\n```clojure\n{:servers\n {:pinboard\n  {:url \"http://127.0.0.1:3030/mcp\"\n   :tools [\"list_bookmarks\" \"search_bookmarks\" \"add_bookmark\"\n           \"delete_bookmark\" \"list_tags\" \"recent_bookmarks\"]}}}\n```\n\n## Tools\n\n| Tool | Description |\n|------|-------------|\n| `list_bookmarks` | List all bookmarks; filter by tag, limit results |\n| `search_bookmarks` | Full-text search across title, description, tags |\n| `add_bookmark` | Create new bookmark with url, title, description, tags |\n| `delete_bookmark` | Delete bookmark by URL |\n| `list_tags` | Get all tags with usage counts |\n| `recent_bookmarks` | Get most recent bookmarks |\n\n## Architecture\n\n### MCP Transport (Streamable HTTP, 2025-03-26)\n\nSingle `/mcp` POST endpoint. Session lifecycle:\n\n1. Client sends `initialize` → server creates session, returns `Mcp-Session-Id` header\n1. Client sends `notifications/initialized` (no response needed, 204)\n1. All subsequent requests include `Mcp-Session-Id` header\n1. Server validates session on every non-initialize request\n\n### Code Structure\n\nEverything lives in `pinboard_mcp.bb` — it's one file, intentionally:\n\n```\nConfiguration / auth loading\n│\nPinboard HTTP client (api-get)\n│\nNormalization helpers (parse-tags, normalize-bookmark)\n│\nTool implementations (tool-*)\n│\nTool registry (tools vector — the schemas the LLM sees)\n│\nTool dispatch (case on name → tool-*)\n│\nJSON-RPC handlers (handle-initialize, handle-tools-list, handle-tools-call)\n│\nHTTP server (http-kit, handler, handle-mcp)\n│\nEntry point (-main)\n```\n\n### Error Handling\n\nTool errors return `{:error true :message \"...\"}` which dispatch-tool wraps in an MCP `isError: true` content block. The LLM sees the error message and can reason about it (e.g., retry with different args, report back to user).\n\nAPI errors (4xx/5xx) are surfaced the same way — they never throw past the tool boundary.\n\n## Development\n\n### Adding a Tool\n\n1. Write `tool-\u003cname\u003e [args config]` function that returns data or `{:error ...}`\n1. Add entry to `tools` vector with `:name`, `:description`, `:inputSchema`\n1. Add case branch in `dispatch-tool`\n1. Add test in `tests/test_pinboard_mcp.clj`\n\n### Testing\n\n```bash\n# Run integration tests (fake API, no real credentials needed)\nbb test\n```\n\nTests use a fake API server that mimics Pinboard's responses. Tests call the real server process via JSON-RPC, exercising the full request/response cycle. No mocks — test like a client would.\n\n### Staging Tests\n\nRun against the real Pinboard API to verify end-to-end functionality:\n\n```bash\n# Requires a real Pinboard API token\nPINBOARD_TEST_TOKEN=\"username:real-token\" bb test-staging\n```\n\n**Warning:** Staging tests modify real data. Use a test account.\n\n- 4 read-only tests: list_bookmarks, search_bookmarks, list_tags, recent_bookmarks\n- 2 read-write tests: add+delete workflows (always cleanup)\n- Rate limiting: 3.5s delay between tests (Pinboard limit: 1 req/3sec)\n- Unique test URLs: `https://www.jupiterbroadcasting.com/test-{uuid}`\n- Unique tags: `staging-test-{uuid}`\n\n### Common Gotchas\n\n**Port 0 allocation:** The server uses port 0 by default (OS assigns). The actual port is in the startup JSON line on stdout. For NixOS services, use a fixed port via `PINBOARD_MCP_PORT`.\n\n**Session validation:** mcp-injector re-initializes sessions on startup (via `warm-up!`). If the server restarts, stale session IDs in mcp-injector will hit a 400. mcp-injector handles this by re-calling `initialize` on 400/401/404 — don't fight it.\n\n**Pinboard API rate limits:** Pinboard limits requests. The API uses `auth_token` in query params. Build in small delays if doing bulk operations.\n\n**Tag normalization:** Pinboard returns tags as a space-separated string. Always convert to set internally (`#{\"ai\" \"tools\"}`).\n\n**Boolean normalization:** Pinboard returns \"yes\"/\"no\" strings. Normalize to true/false booleans.\n\n**search-bookmarks is client-side:** Pinboard doesn't have a search API. The MCP server fetches bookmarks and filters in Clojure. Use `limit` parameter to avoid fetching thousands of bookmarks.\n\n## Technical Notes\n\n**Cache Behavior:** The server caches bookmark data to reduce API calls. Cache is refreshed when:\n\n- 60 seconds have passed since last check (`cache-ttl-ms`)\n- A bookmark is added or deleted (cache is invalidated immediately via `:checked-at 0`)\n\n**Stale Fallback:** If the Pinboard API is unreachable (network error, timeout, rate limit), the server will return stale cached data if available rather than failing. This ensures continued operation during API outages.\n\n**Rate Limiting:** Pinboard allows ~1 request per 3 seconds. The server implements a single retry with 5-second backoff on HTTP 429 (rate limit) responses. For bulk operations, add delays between requests.\n\n**Concurrency:** Session management uses atomic compare-and-set operations to avoid blocking. The bookmark fetch has an in-flight gate to prevent duplicate API calls when multiple requests arrive simultaneously.\n\n## Philosophy\n\nFollow grumpy pragmatism:\n\n- **Actions, Calculations, Data** — tool functions are actions, keep them thin; pure extraction/formatting logic lives in `-row` helpers or inline maps\n- **One file is fine** — don't split into namespaces until you genuinely need to\n- **No abstractions until they hurt** — the dispatch `case` is fine, resist the urge to make it data-driven\n- **Test against real services** — mock drift kills confidence\n- **YAGNI** — resources/prompts MCP extensions not implemented because they're not needed yet\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnoblepayne%2Fpinboard-mcp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnoblepayne%2Fpinboard-mcp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnoblepayne%2Fpinboard-mcp/lists"}