feat: set_presence REST endpoint, MCP tool, and shared PresenceStatus enum#33
Merged
Conversation
…P-05
Add REST endpoints for reading any user's profile and batch-resolving
display names, MCP tools for agent profile access, kind:0 side-effect
sync from Nostr metadata events, and a working NIP-05 identity endpoint.
REST API:
- GET /api/users/{pubkey}/profile — read any user's profile (auth required)
- POST /api/users/batch — batch resolve display names (max 200, dedup,
case-normalize; invalid-length inputs returned in `missing`)
MCP tools:
- get_user_profile — read own or any user's profile by pubkey
- get_users_batch — batch resolve display names for message attribution
Kind:0 side effect:
- NIP-01 profile metadata events now sync to the users table
- Absolute-state semantics: absent fields are cleared (set to NULL)
- Fields: display_name (with name fallback), avatar_url (picture/image),
about, nip05_handle
- Empty strings converted to NULL via empty_to_none() to respect the
UNIQUE constraint on nip05_handle
NIP-05 (/.well-known/nostr.json):
- Replaces empty stub with working identity verification
- Exact domain match against relay_url (no LIKE wildcards)
- Access-Control-Allow-Origin: * header (NIP-05 spec requirement)
- No authentication required (public discovery endpoint)
DB changes:
- update_user_profile gains nip05_handle parameter
- New get_user_by_nip05 function (exact match, not LIKE)
- No schema migrations required
Tests:
- 12 new REST API e2e tests (profile CRUD, batch, NIP-05, auth, edges)
- 3 new MCP e2e tests (self profile, other profile, batch)
- Tool count assertion updated from 36 to 38
- All 55 e2e tests pass (32 REST + 10 MCP + 13 relay)
- Update MCP tool count from 36 to 38 across all references - Add get_user_profile and get_users_batch to Profile tools table - Update integration test counts: 32 REST, 10 MCP, 55 total (plus 7 workflow tests noted separately) - Add B-8 exercise: Bob tests profile resolution via MCP tools - Add C-8 exercise: Charlie verifies NIP-05 identity endpoint - Add C-9 exercise: Charlie tests profile lookup edge cases - Update A-5 to set Alice's NIP-05 handle (alice@localhost) - Update Expected Results table with profile and NIP-05 rows - Update 'as of' date to 2026-03-11
…, update TESTING.md - Add 'search' MCP tool wrapping GET /api/search (Typesense full-text) - Add 'get_presence' MCP tool wrapping GET /api/presence (bulk lookup) - Fix set_profile to support nip05_handle field (was silently dropped) - Thread nip05_handle through UpdateProfileBody → update_user_profile - Update TESTING.md section 6 tool table: correct all tool names, add missing tools (40 total), remove nonexistent tools - Fix exercises A-5, B-6 for accuracy; add bootstrap channel timing note - Fix expected results: Charlie in profiles map (not missing list) - Update e2e test assertions: 38 → 40 tools
…ests Crossfire R1 + R2 fixes: - Validate nip05_handle format (user@domain) and restrict domain to relay's own domain on both REST and kind:0 ingestion paths - Return 409 Conflict on duplicate nip05_handle (was generic 500) - Allow empty string to clear nip05_handle (empty → NULL via DB layer) - Kind:0 side effect now validates NIP-05 format and domain before syncing to users table (invalid handles silently cleared) - Add percent_encode to get_presence MCP tool for consistency - Add 3 NIP-05 integration tests: round-trip set+lookup, clear handle, duplicate handle conflict - Update doc comments to include nip05_handle - Make extract_domain pub(crate) for reuse across modules
…lience Crossfire R3 fixes: - Extract shared canonicalize_nip05() in nip05.rs — lowercases local+domain, validates format and relay domain match, used by both REST and kind:0 paths - Batch endpoint: 64-char non-hex inputs now correctly reported in missing list instead of silently dropped - Kind:0 duplicate NIP-05: retry profile sync without contested handle so display_name/about/avatar_url are still written (UNIQUE violation no longer drops all profile fields) - Add kind:0 NIP-05 regression test: valid handle syncs, off-domain cleared - Update batch test to cover non-hex 64-char inputs
… enum
Add PUT /api/presence so GUI clients and agents can set presence without
a WebSocket connection. The existing GET endpoint and kind:20001 WebSocket
path are unchanged.
REST (sprout-relay):
- PUT /api/presence accepts {"status": "online"|"away"|"offline"}
- Returns {"status": "...", "ttl_seconds": N} (90 for online/away, 0 for offline)
- "offline" calls clear_presence (Redis DEL); others call set_presence (Redis SET EX 90)
- Auth via extract_auth_pubkey — users can only set their own presence
- Custom ApiJson<T> extractor returns standard {"error": "..."} envelope on
deserialization failure, preserving Axum's original status codes (400/415/422)
MCP (sprout-mcp):
- New set_presence tool (#41) with typed PresenceStatus enum in JSON schema
- relay_client error propagation improved: all HTTP methods now include
status code, URL, and response body in error messages
Shared types (sprout-core):
- PresenceStatus enum (Online/Away/Offline) defined once in sprout-core
- schemars::JsonSchema derived conditionally via mcp-schema feature flag
- Imported by both sprout-relay and sprout-mcp — single source of truth
- Includes Display, as_str(), serde roundtrip, and 4 unit tests
Testing:
- 5 new REST e2e tests: set online, away+offline transitions, invalid
status (422 + envelope), auth required (401 + body), missing field (422)
- 4 new MCP e2e tests: set online, offline, away (all with TTL assertions),
invalid status
- Tool count updated 40 -> 41 with JSON-parsed assertions throughout
- All tests verify ttl_seconds in PUT responses
- cargo fmt, cargo clippy, cargo check all clean
28c57aa to
2003dba
Compare
* origin/main: Plumb desktop profile identity UI (#32) Add unread indicators to desktop channels (#31) feat: public profiles, NIP-05 identity, search & presence tools (#28) Add desktop settings page (#30) Remove OS titlebar and add custom window chrome (#29) # Conflicts: # TESTING.md # crates/sprout-mcp/src/server.rs # crates/sprout-test-client/tests/e2e_mcp.rs
wpfleger96
added a commit
that referenced
this pull request
May 22, 2026
The zizmor scanner runs as an org-level GHAS integration, not from a repo checkout — it never reads .github/zizmor.yml. Dismissed the cache-poisoning alerts (#10-13, #32, #33) as false positives via the code-scanning API instead, with references to the upstream bug (zizmorcore/zizmor#2051).
wpfleger96
added a commit
that referenced
this pull request
May 22, 2026
The zizmor scanner runs as an org-level GHAS integration, not from a repo checkout — it never reads .github/zizmor.yml. Dismissed the cache-poisoning alerts (#10-13, #32, #33) as false positives via the code-scanning API instead, with references to the upstream bug (zizmorcore/zizmor#2051).
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
PUT /api/presenceso GUI clients and agents can set their presence status without requiring a WebSocket connection. Introduces a sharedPresenceStatusenum insprout-coreas the single source of truth, used by both the relay REST API and the MCP agent surface.Implements the
set_presenceMCP tool that was explicitly deferred in #28:What Changed
Shared Types (
sprout-core)PresenceStatusenum (Online,Away,Offline) defined once insprout-core::presenceschemars::JsonSchemaderived conditionally viamcp-schemafeature flag — zero overhead for non-MCP consumersDisplay,as_str(),PartialEq/Eq, and serderename_all = "lowercase"as_str,DisplayREST API (
sprout-relay)PUT /api/presence— accepts{"status": "online" | "away" | "offline"}{"status": "...", "ttl_seconds": N}(90 for online/away, 0 for offline)"offline"callsclear_presence()(Redis DEL); others callset_presence()(Redis SET EX 90)extract_auth_pubkey— users can only set their own presenceApiJson<T>custom extractor — wrapsaxum::Json<T>to return standard{"error": "..."}envelope on deserialization failure, preserving Axum's original status codes (400/415/422)MCP Tools (
sprout-mcp)set_presencetool (fix: just reset race condition #41) with typedPresenceStatusenum in the JSON schema — gives LLM callers a tight, validated schema instead of a free-form stringRelayClientGET/POST/PUT/DELETE/get_api now include status code, URL, and response body in error messagesTESTING.md
set_presenceTesting (9 new e2e tests + 4 unit tests)
cargo fmt,cargo clippy,cargo checkall cleanFiles Changed (13 files, +560/-30)
crates/sprout-core/src/presence.rsPresenceStatusenum, single source of truthcrates/sprout-core/Cargo.tomlschemarsoptional dep,mcp-schemafeaturecrates/sprout-core/src/lib.rspub mod presence,pub use PresenceStatuscrates/sprout-mcp/Cargo.tomlmcp-schemafeature on sprout-corecrates/sprout-mcp/src/server.rsset_presencetool using shared enumcrates/sprout-mcp/src/relay_client.rscrates/sprout-relay/src/api/presence.rsset_presence_handlerwith TTL responsecrates/sprout-relay/src/api/mod.rsApiJson<T>custom extractorcrates/sprout-relay/src/router.rsGET(...).PUT(...)on/api/presencecrates/sprout-test-client/tests/e2e_mcp.rscrates/sprout-test-client/tests/e2e_rest_api.rsTESTING.mdCargo.lockDesign Notes
PresenceStatusenum. This divergence is documented in the handler doc comment.PUTis the correct verb — presence is a single-field resource being fully replaced.ttl_secondslets clients know when to send the next heartbeat without hardcoding the value.ApiJson<T>preserves Axum's original status codes while wrapping rejections in the standard error envelope, so 415 (wrong Content-Type), 400 (malformed JSON), and 422 (unknown enum variant) all get the right status code with a consistent body format.schemarsis optional insprout-corebehindmcp-schema, keeping the zero-I/O-dependency principle intact for non-MCP consumers.Test Results
All 68 integration tests pass. 4 sprout-core unit tests pass.