feat: social notes MCP + CLI tools (kind:1/kind:3)#268
Merged
tlongwell-block merged 9 commits intomainfrom Apr 8, 2026
Merged
Conversation
Add 5 MCP tools and 5 CLI commands to expose kind:1 (text note) and
kind:3 (contact list) global Nostr events. Includes relay bug fix for
GET /api/events/{id} allowlist, two new REST endpoints, SDK builders,
and E2E integration tests.
Implements the social toolset for agent tweeting via ACP harness.
Add 4 tests covering the happy paths that were missing: - UsersRead grants access to kind:0 (profile) - UsersRead grants access to kind:3 (contact list) - MessagesRead grants access to kind:30023 (long-form) - Exhaustive routing test: each kind maps to exactly one scope, unknown kinds denied by both scopes
- Add 10 scope-enforcement unit tests in events.rs covering the kind-based allowlist (user data vs message kinds, closed-default, empty scopes, both scopes) - Escape LIKE metacharacters in search_users to prevent wildcard injection DoS (pre-existing issue) - Add 200-char length cap on search query parameter - Add limit floor (clamp 1..100) in get_user_notes - Fix toolsets.rs comment style to match section convention - Add #[serde(default)] on CLI ContactEntry fields
Verify that %, _, and \ are properly escaped to prevent wildcard injection. Tests run without Postgres — pure string logic.
…e refactor - Document search_users limit cap (max 50) in SearchUsersQuery - Enforce limit cap at REST layer (.min(50)) - Document get_user_notes response shape (tags/sig omitted) - Update MCP get_user_notes description to note stripped response - Standardize hex::decode → nostr_hex::decode in users.rs - Add global_only field to EventQuery for defensive channel filtering - Refactor escape_like into named function, tests use production fn - Expand ORDER BY comment documenting global behavioral change
…onstants, tool docs - Add MAX_CONTACTS (10,000) cap in build_contact_list for DoS defense - Deduplicate contact list entries by pubkey (first occurrence wins) - Extract scope allowlist constants to module level (eliminates shadow tests) - Improve get_event MCP tool description with explicit scope requirements - Add publish_note content size pre-validation at MCP layer - Add social toolset documentation comment - Add tests: empty note content, duplicate contacts, contact list cap
Without this, the MCP server always starts with only the default toolset, even when the operator sets SPROUT_TOOLSETS=default,social on the ACP harness. The env var is now forwarded in build_mcp_servers() so the MCP server enables the same toolsets.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Add 5 MCP tools and 5 CLI commands to expose kind:1 (text note) and kind:3 (contact list) global Nostr events that PR #245 already stores in the relay. Enables agents in the ACP harness to tweet.
What's included
Relay
GET /api/events/{id}now returns global events (kind:0, 1, 3, 30023) with a scope-aware allowlist instead of 404ing all global events. Returns 404 (not 403) for unauthorized kinds to prevent event enumeration.GET /api/users/{pubkey}/notes(paginated kind:1 listing) andGET /api/users/{pubkey}/contact-list(latest kind:3)search_users, 200-char search query cap,limitfloor/ceiling enforcementSDK
build_note(content, reply_to)— kind:1 text note builder with 64 KiB content limitbuild_contact_list(contacts)— kind:3 contact list builder with pubkey/relay_url/petname validationSdkError::InvalidInput— new error variant for input validation failuresMCP (opt-in
socialtoolset)publish_note— publish kind:1 text note via WebSocketset_contact_list— replace kind:3 contact list via WebSocketget_event— fetch any global event by ID (kind:0/1/3/30023)get_user_notes— list user's kind:1 notes with composite cursor paginationget_contact_list— fetch user's latest kind:3 contact listCLI
publish-note,set-contact-list,get-event,get-user-notes,get-contact-listDB
before_id) for stable same-second paginationget_latest_global_replaceablequery with NIP-16 canonical orderingORDER BY created_at DESC, id ASCtiebreaker (global change toquery_events)global_onlyfilter onEventQueryfor defensive channel isolationTests
Architecture
Safety invariants
GET /api/events/{id}uses conditional scope checks —UsersReadfor kind:0/3,MessagesReadfor kind:1/30023, 404 for everything else (fail-closed)channel_id = NULLby the ingest invariantBreaking changes
query_eventsnow usesORDER BY created_at DESC, id ASC(wascreated_at DESConly). This adds a deterministic tiebreaker for same-second events. Existing callers (canvas, WebSocket REQ) now get stable ordering instead of arbitrary row selection.Deferred
follow/unfollowconvenience commandsGET /api/notes)Files changed (15)
sprout-sdk/src/lib.rsSdkError::InvalidInputsprout-sdk/src/builders.rsbuild_note+build_contact_list+ 11 testssprout-relay/src/api/events.rssprout-relay/src/api/users.rssprout-relay/src/api/mod.rssprout-relay/src/router.rssprout-db/src/event.rsget_latest_global_replaceable+global_onlysprout-db/src/lib.rssprout-db/src/user.rssprout-mcp/src/server.rssprout-mcp/src/toolsets.rssprout-cli/src/main.rssprout-cli/src/commands/social.rssprout-cli/src/commands/mod.rspub mod socialsprout-test-client/tests/e2e_rest_api.rs