fix(mcp): send threaded replies via WebSocket to prevent self-mention echo#115
Merged
tlongwell-block merged 3 commits intomainfrom Mar 19, 2026
Merged
fix(mcp): send threaded replies via WebSocket to prevent self-mention echo#115tlongwell-block merged 3 commits intomainfrom
tlongwell-block merged 3 commits intomainfrom
Conversation
… echo Unify send_message to use WebSocket for all messages. Previously, threaded replies went through REST, causing the relay to sign them with its own key. This made ignore_self fail, and the bot would receive its own reply as a new @mention.
…ear stale API token, correct tool count spawn_mcp_server now: - Sets SPROUT_TOOLSETS=all so all 42 tools are available - Removes SPROUT_API_TOKEN to prevent stale tokens from .env causing NIP-42 auth failures against a fresh database - Tool count assertion updated from 43 to 42 (matches ALL_TOOLS)
51b8833 to
a277f7a
Compare
tokio-tungstenite pulls in rustls but no CryptoProvider is installed at process startup, causing a panic even for plain ws:// connections. Add rustls as a direct dependency and call install_default() in main.
a277f7a to
b01ab37
Compare
tlongwell-block
added a commit
that referenced
this pull request
Mar 19, 2026
…postgres * origin/main: fix(mcp): send threaded replies via WebSocket to prevent self-mention echo (#115)
tlongwell-block
added a commit
that referenced
this pull request
Mar 19, 2026
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.
Problem
When a bot replies in a thread, it receives its own reply back as a new
@mention, creating an echo loop.Root cause chain:
Top-level messages didn't echo because MCP sent them via WebSocket, signed by the bot's own key, so
ignore_selfcaught them.Fix
Unify
send_messageto use WebSocket for all messages — top-level and replies. The bot signs every event with its own key, soignore_selfworks universally.What changed (
crates/sprout-mcp/src/server.rs)build_top_level_message_tags→build_message_tags— same logic, now used for all messagesfind_root_from_tags— pure function that parses a parent event's NIP-10 tags to find the thread root. Returnsroot.or(reply)(prefers explicit"root"marker, falls back to"reply"target which IS the root for direct replies)build_reply_tags— async method that fetches the parent event viaGET /api/events/{id}, parses its tags, and constructs NIP-10etags:["e", parent, "", "reply"]["e", root, "", "root"]+["e", parent, "", "reply"]send_message— all messages go through WebSocket["broadcast", "1"]tag whenbroadcast_to_channel=trueon repliesWhat this keeps
normalize_mention_pubkeys(self-mention filtering for explicit mentions)mention_pubkeysandkindoverride support on repliesWhat this removes
POST /api/channels/{ch}/messagescall fromsend_messageif p.parent_event_id.is_none()branch splitNot in scope
send_diff_message(kind:40008) — always uses REST, even for top-level diffs. Separate echo vector, separate fix.FilterContext::from_eventinfilter.rs—authorfield uses rawevent.pubkey, wrong for relay-signed events. Lower priority.Testing
Automated
cargo test -p sprout-mcpe2e_rest_apie2e_relaycargo clippyclean,cargo fmtcleanLive E2E with ACP harnesses
Three agents (Alice, Bob, Charlie) running via
sprout-acpharness against a live relay, exercising the actual echo-fix path:b848ceb6...["e", root, "", "reply"]✅1f821603...["e", root, "", "reply"]✅25623e53...["e", root, "", "reply"]✅Thread metadata: 3 replies, 3 participants, correct ancestry. Zero echo across all agents.
Crossfire review
Additional fixes (in this PR)
crates/sprout-test-client/tests/e2e_mcp.rsspawn_mcp_servernow setsSPROUT_TOOLSETS=all(was missing → only 25/42 tools available)spawn_mcp_servernow removesSPROUT_API_TOKENfrom subprocess env (stale token from.envcaused NIP-42 auth failures against fresh DBs)ALL_TOOLS)crates/sprout-test-client/src/bin/mention.rsrustls::crypto::ring::default_provider().install_default()—tokio-tungstenitepulls in rustls but no CryptoProvider was installed, causing a panic even for plainws://connections