Files
2026-10-22 21:33:25 +01:00

25 KiB
Raw Permalink Blame History

SimpleX MCP server: flows

Sequence diagrams of the main flows, each with notes and source locations.

Related specs: README | Architecture | API

Participants:

Letter Participant
P the person at the terminal
A the agent in Claude Code
H the HTTP listener with the MCP SDK
T AgentTools
X Actions
K Control
C the SimpleX core behind ChatApi
O the owner in the SimpleX app
D a peer

#G is the agent control chat, USER the agent's user ID, LINK the link as connFullLink [connShortLink]. Chat commands are shown as the server sends them. Card and progress texts with a semicolon are shown with a comma in the diagrams; the exact texts are in the notes.


Table of contents

  1. First start and the owner joins
  2. register
  3. connect with approval
  4. send with responses
  5. receive with wait
  6. send under a standing approval, and /revoke
  7. leave
  8. Mode host
  9. Expiry
  10. A lost call and its re-attachment
  11. Shutdown with pending actions
  12. --owners setup

1. First start and the owner joins

sequenceDiagram
  participant P as Person
  participant S as serve.ts
  participant C as Core
  participant K as Control
  participant O as Owner
  P->>S: simplex-mcp serve my-agent --display-name "My Agent"
  S->>S: DATA mode 0700, lock, token, headers.json
  S->>C: ChatApi.init DATA/simplex
  S->>P: version line, data folder, approval mode
  S->>C: /user (no active user)
  S->>C: /_create user {"profile":{"displayName":"My Agent","fullName":"","peerType":"bot"},...}
  S->>C: /network socks=off smp-proxy=always smp-proxy-fallback=protected
  S->>C: startChat, events buffered
  S->>C: /_groups USER (no control marker)
  S->>C: /_group USER {My Agent, history on, menu pending and list}
  S->>C: /_set custom #G {"simplexMcp":{"control":true,"nextAction":1}}
  S->>P: Agent control chat "My Agent" created.
  S->>K: load
  K->>C: /_members #G
  S->>S: HTTP listens on 127.0.0.1:5226, /mcp answers 503 until the owner setup ends
  S->>S: server.json
  K->>C: /_delete link #G (groupLinkNotFound ignored)
  K->>C: /_create link #G member
  K->>P: join line, QR code, link, QR hint, owner line
  S->>P: Listening on http://127.0.0.1:5226/mcp
  O->>C: joins by the link
  C->>S: joinedGroupMember
  S->>K: onEvent, queued
  K->>C: /_members #G
  K->>C: /_delete link #G
  K->>C: /_member role #G 3 owner
  K->>C: /_members #G
  K->>O: I'm your agent. I ask here before I connect or send.
  K->>P: Alice joined (member 3) and is now an owner. The link is deleted.
  K->>P: Add the agent to Claude Code: npx -y @simplex-chat/mcp register my-agent

Notes:

  • At first use, libsimplex and the Node.js add-on are downloaded, with one Downloading ... line each on stderr (libs.ts:112-133).
  • With --data, --data <folder> is appended to the register hint, shell-quoted (serve.ts:164-166).
  • With --qr-code small, the line If the QR code does not scan, ... is printed after the link (control.ts:307).
  • The server runs ownerJoined for every joinedGroupMember event of a human, and the link is deleted in that handler (control.ts:152-153, :266-273).

Source: cli.ts:191-200, serve.ts:64-123, files.ts:12-44, control.ts:374-385, :88-103, :236-240, :301-311, :146-155.


2. register

sequenceDiagram
  participant P as Person
  participant R as register.ts
  participant L as claude CLI
  participant CC as Claude Code
  participant H as HTTP
  P->>R: simplex-mcp register my-agent [--scope local] [--data DIR]
  R->>R: read DATA/server.json
  alt server.json missing
    R->>P: Agent "my-agent" was never started. Start it with: ... (exit 1)
  end
  R->>L: claude mcp remove --scope user simplex-my-agent
  R->>L: claude mcp add-json --scope user simplex-my-agent {"type":"http","url":"http://127.0.0.1:5226/mcp","headersHelper":"cat DATA/headers.json","timeout":169800000}
  alt claude not found
    R->>P: Add this entry under "mcpServers" in ~/.claude.json: {...} (exit 1)
  else add-json exit status other than 0
    R->>P: claude mcp add-json failed: STDERR (exit 1)
  end
  R->>P: Added to Claude Code: scope user, server simplex-my-agent.
  CC->>CC: headersHelper: cat DATA/headers.json
  CC->>H: POST /mcp with Authorization: Bearer TOKEN

Notes:

  • simplex-mcp ignores the exit status of claude mcp remove and stops at that step only when the claude binary is missing (register.ts:35-36).
  • timeout is (expireHours + 24) × 3600000 + 600000 ms from server.json (register.ts:48).
  • With --scope local, the project folder in the manual entry is the git repository root, the main repository of a worktree, or the current folder outside a repository (register.ts:53, :57-81).

Source: cli.ts:103-118, :200, register.ts:30-87, files.ts:50-53.


3. connect with approval

sequenceDiagram
  participant A as Agent
  participant H as HTTP and SDK
  participant T as AgentTools
  participant X as Actions
  participant K as Control
  participant C as Core
  participant O as Owner
  participant D as Peer
  A->>H: tools/call connect {target: "@directory.simplex", wait, idle}
  H->>T: validated input, abort signal, progress token
  T->>X: find(key): none
  T->>C: /_connect plan USER @directory.simplex
  C-->>T: connectionPlan: contactAddress ok, planSimplexName
  T->>T: classifyPlan: prepareContact with domain
  T->>X: create(key, connect fields)
  X->>K: refusal: none
  X->>K: saveCounter: /_set custom #G
  X->>K: postCard
  K->>O: I want to connect to bot X (@directory.simplex), as myself. /'approve N' or /'reject N'
  T->>X: decided(action, call)
  X-->>A: progress: Action N waits for an approver until TIME.
  O->>C: /approve N
  C->>K: newChatItems
  K->>X: approve(action, "Alice")
  X->>T: performConnect
  T->>C: /_connect plan USER @directory.simplex
  T->>C: /_prepare contact USER LINK domain=directory.simplex {link data}
  T->>C: /_connect contact @C incognito=off
  C-->>T: startedConnectionToContact
  D->>C: accepts the request
  C->>T: contactConnected: markConnected, collect @C
  T-->>X: perform ends
  X->>K: outcome
  K->>O: card edited: I connected to bot X (@directory.simplex), as myself. Approved by Alice.
  T->>C: /_get chat @C count=50
  T->>T: wait until idle or startedAt + wait
  T->>C: /_read chat items @C, new item IDs
  T->>X: remove(action)
  T-->>A: {action: N, status: done, chat: @C, connected: true, messages}

Notes:

Plan Commands
known prepared contact or group /_connect contact @C incognito=X [json M]
/_connect group #G incognito=X [json M]
channel link (direct false, with link data) /_prepare group USER LINK direct=off [domain=D] JSON
/_connect group #G incognito=X
contact address with link data, and a SimpleX name or a message /_prepare contact USER LINK [domain=D] JSON
/_connect contact @C incognito=X [json M], or /_connect group #G ... for a business
contactViaAddress /_connect contact USER incognito=X CONTACT_ID
other links /_connect USER incognito=X LINK
  • After a failed prepared connect, the server deletes the prepared chat with notify=off (tools.ts:544-551).
  • The connection is matched by contactConnected or userJoinedGroup: by contact or group ID, or by the pending connection ID (tools.ts:387-395, :557-562).
  • In the perform step the server waits for the other side until startedAt + wait, at most until created + 23 h 50 min, and then reports connected: false (tools.ts:483, :507-511).
  • Without message, the idle time is counted from the connection; with message, from the first reply (tools.ts:568, messages.ts:225).
  • For a target the agent is connected to, the result is status: known with its chat fields, and no action is created (tools.ts:417).
  • Exact progress texts: Action N waits for an approver until TIME., then Action N waits for the connection until TIME., then K messages in CHAT; waiting until TIME. (actions.ts:296-299, tools.ts:452).

Source: tools.ts:412-464, :466-571, :811-874, actions.ts:119-183, control.ts:134-190.


4. send with responses

sequenceDiagram
  participant A as Agent
  participant T as AgentTools
  participant X as Actions
  participant K as Control
  participant C as Core
  participant O as Owner
  participant D as Peer
  A->>T: send {chat: #15, text, reply_to, mention: [44], wait, idle, limit}
  T->>C: /_get chat #15 count=0
  T->>C: /_get chat #15 around=REPLY_TO count=0
  T->>C: /_members #15
  T->>K: standingApprover(chatInfo): none
  T->>X: create(key, send fields)
  X->>K: card size check and approvers
  K->>O: I want to send a message to group X: `text` replying to ... mentioning @Switchback /'approve #15 N', /'approve #15 all' or /'reject #15 N'
  O->>C: /approve #15 N
  K->>X: approve(action, "Alice")
  X->>T: performSend
  T->>T: collect #15 items with ID above the sent item
  T->>C: /_send #15 json [{msgContent, mentions, quotedItemId}]
  C-->>T: sent item S
  D->>C: replies S+1, S+2
  C->>T: newChatItems: collected
  X->>K: outcome
  K->>O: card edited: I sent a message to group X: `text` ... Approved by Alice.
  T->>T: wait until idle seconds after the last reply, or sentAt + wait
  T->>C: /_read chat items #15 S+1,S+2
  T->>X: remove(action)
  T-->>A: {action: N, status: done, chat: #15, item: S, responses, more: 0}

Notes:

  • Checks before the action: chat reference, role observer or relay refused, reply_to item present, mentioned members present and the chat a group (tools.ts:573-581).
  • The server starts collecting before the send and, after it, drops items with IDs up to the sent item (tools.ts:612, :622-624).
  • Mentioned names missing from the text are prefixed as @name (tools.ts:614-615).
  • While a collected message is live, the server waits for its final update; live items are left out of responses and counted in more (messages.ts:224-225, tools.ts:599, :605).
  • At most 100 replies are kept per action; later ones are counted as overflow (messages.ts:78-79).

Source: tools.ts:573-629, messages.ts:71-83, :222-230, control.ts:448-453.


5. receive with wait

sequenceDiagram
  participant A as Agent
  participant T as AgentTools
  participant W as ChatItemsWatch
  participant C as Core
  participant D as Peer
  A->>T: receive {chat: #12, wait: 3600, limit: 50}
  T->>W: collect #12 into arrived
  T->>C: /_get chat #12 initial=1
  C-->>T: chatStats.unreadCount 0
  T-->>A: progress: No new messages in #12, waiting until TIME.
  D->>C: post, item 40
  C->>W: newChatItems: arrived
  T->>C: /_read chat items #12 40
  T->>W: stop collecting
  T-->>A: {chat: #12, messages: [40], more: 0}

Notes:

  • With unread items the server reads around=<minUnreadItemId> count=0, then pages of after=<last> count=100 until it holds limit + 1 unread, non-live messages or a page is short (messages.ts:183-194).
  • Unread items other than messages in the read pages are marked read with the returned messages (tools.ts:663, :680).
  • more: the arrived messages not returned, the overflow and the unread items not read (tools.ts:682).
  • Items received while the pages are read are returned once (tools.ts:662-665).
  • Exact progress text: No new messages in CHAT; waiting until TIME. (tools.ts:669).

Source: tools.ts:653-686, messages.ts:183-220.


6. send under a standing approval, and /revoke

sequenceDiagram
  participant A as Agent
  participant T as AgentTools
  participant X as Actions
  participant K as Control
  participant C as Core
  participant O as Owner
  O->>C: /approve #9 all (on a pending send card)
  C->>K: newChatItems
  K->>C: /_get chat #9 count=0
  K->>C: /_set custom #9 {"simplexMcp":{"approvedBy":"Alice","approvedById":3,"approvedAt":"..."}}
  K->>X: approve each pending send to #9 with a card
  K->>O: Approved for business X (#9) by Alice. /'revoke #9'
  A->>T: send {chat: #9, text}
  T->>C: /_get chat #9 count=0
  T->>K: standingApprover: Alice
  T->>X: create(key, fields, "Alice"): performing, standing
  X->>K: saveCounter (no card)
  X->>T: performSend
  T->>C: /_send #9 json [...]
  X->>K: outcome
  K->>O: I sent a message to business X: `text` Approved for this chat by Alice. /'revoke #9'
  T-->>A: {action, status: done, chat: #9, item, responses, more}
  O->>C: /revoke #9
  K->>C: /_get chat #9 count=0
  K->>C: /_set custom #9 {other keys}
  K->>O: Messages to business X (#9) need approval again.

Notes:

Source: control.ts:192-211, tools.ts:586, actions.ts:119-146, :223-240.


7. leave

sequenceDiagram
  participant A as Agent
  participant T as AgentTools
  participant X as Actions
  participant K as Control
  participant C as Core
  participant O as Owner
  A->>T: leave {chat: #9}
  T->>X: find(key): none
  T->>C: /_get chat #9 count=0
  T->>X: create(key, leave fields)
  K->>O: I want to leave business X (#9). /'approve #9 N' or /'reject #9 N'
  O->>C: /approve #9 N
  K->>X: approve(action, "Alice")
  X->>T: performLeave
  T->>C: /_get chat #9 count=0
  T->>C: /_leave #9 (membership current or pending)
  T->>C: /_delete #9 full
  X->>K: outcome
  K->>O: card edited: I left business X (#9). Approved by Alice.
  T-->>A: {action: N, status: done, chat: #9}

Notes:

  • For a contact, the card text is I want to delete KIND NAME (@N)., and the perform step sends /_delete @N full (actions.ts:280-282, tools.ts:647).
  • The delete mode is full without notify=off, so the core notifies the contact or the members (types.js:76-81).
  • The server looks for an attached action before it reads the chat, so a repeated call after the deletion is attached to the done action (tools.ts:633-636, messages.test.ts:745-759).

Source: tools.ts:631-651, control.ts:454-457.


8. Mode host

sequenceDiagram
  participant P as Person
  participant A as Agent
  participant H as HTTP and SDK
  participant T as AgentTools
  participant X as Actions
  participant K as Control
  participant O as Owner
  Note over P,H: the person started serve my-agent --approve host
  A->>H: tools/list
  H-->>A: connect, send, leave with _meta {"anthropic/requiresUserInteraction": true}
  A->>A: Claude Code asks its user
  A->>T: send {chat: @4, text}
  T->>X: create: performing
  X->>K: saveCounter (no card)
  X->>T: performSend
  X->>P: Action N: performed without approval (mode host).
  X->>K: outcome
  K->>O: I sent a message to bot X: `text` Performed without approval (mode host).
  T-->>A: {action: N, status: done, ...}

Notes:

  • New actions are still refused unless the control chat has an approver (control.ts:107-110).
  • In mode host, undefined is returned by standingApprover, so standing approvals are ignored by send, list and /list (control.ts:115).
  • _meta is set on connect, send and leave only (tools.ts:361).

Source: tools.ts:355-365, actions.ts:130, :238, control.ts:419.


9. Expiry

sequenceDiagram
  participant A as Agent
  participant X as Actions
  participant K as Control
  participant O as Owner
  A->>X: send call creates pending action N at T
  K->>O: card N
  Note over X: expires = T + expireHours
  loop every 60 s, and before each owner command
    X->>X: expireDue
  end
  X->>X: pending and due: expired
  X->>K: outcome
  K->>O: card edited: ... Expired.
  X-->>A: {action: N, status: expired}
  X->>X: removed with the result
  O->>K: /approve N
  K->>O: Action N is not pending.

Notes:

  • A decided action with an open count of 0 is removed at the first tick after its expiry time (actions.ts:160-161).
  • Expiry times are listed in the /pending reply (control.ts:472-474).
  • The server ends the connect wait at created + 23 h 50 min, within the 24 hours in which a message can be edited (tools.ts:112, :483).

Source: actions.ts:97, :153-164, control.ts:164, :433-434.


10. A lost call and its re-attachment

sequenceDiagram
  participant A as Agent
  participant H as HTTP and SDK
  participant T as AgentTools
  participant X as Actions
  participant O as Owner
  A->>H: send {chat: @4, text: "t"}
  H->>T: call
  T->>X: create: pending, open 1
  A->>H: closes the response stream
  H->>T: ctx.mcpReq.signal aborted
  T->>X: decided: false, open 0
  Note over H: result discarded by the SDK
  A->>T: list
  T-->>A: actions: [{action: N, tool: send, input, state: pending, open: false}]
  O->>X: /approve N
  X->>X: perform, done, replies collected
  A->>T: send {chat: @4, text: "t"}
  T->>X: find(key): action N
  T->>T: doneAtCall: wait counted from now
  T->>X: remove(action)
  T-->>A: {action: N, status: done, item, responses, more}

Notes:

Source: tools.ts:431-442, :590-608, :694, actions.ts:166-177, messages.ts:206-220.


11. Shutdown with pending actions

sequenceDiagram
  participant P as Person
  participant S as serve.ts
  participant X as Actions
  participant K as Control
  participant H as HTTP
  participant C as Core
  participant O as Owner
  P->>S: Ctrl-C (SIGINT)
  S->>S: stopped: events ignored
  par actions
    S->>X: stop()
    X->>X: stopping, timer cleared, cards being posted awaited
    X->>X: pending and performing: interrupted
    X->>K: outcome of each
    K->>O: pending: ... Cancelled: the server stopped.
    K->>O: performing: ... Approved by Alice, interrupted: the server stopped.
  and HTTP
    S->>H: close, closeAllConnections (open calls aborted)
  end
  S->>P: Cancelled 2 actions.
  S->>C: /sql chat: agent connection IDs of #G members
  S->>C: /sql agent: count of pending deliveries, every 100 ms, at most 10 s
  S->>C: /_stop, close the store
  S->>S: remove DATA/lock
  S->>P: Stopped.
  Note over P,O: next start: counter continues, /approve of an old number: Action N is not pending.

Notes:

  • The interrupted card line is DECIDER; interrupted: the server stopped. (control.ts:426).
  • A host-mode action whose counter is being written at the stop is set to pending and cancelled without a perform (actions.ts:226, actions.test.ts:380-391).
  • After a crash, the cards of that run are left with their commands; /approve of their numbers gets the reply Action N is not pending. (control.ts:182).
  • The stop signal of the performs is aborted after the actions are marked interrupted: a connect waiting for its connection ends, and an interrupted perform logs no line (actions.ts:218, :252, tools.ts:507-509).
  • An error of a /sql query of the delivery wait is logged as delivery wait: ERROR on stderr and ends the wait (serve.ts:185).

Source: serve.ts:48-62, :150-176, actions.ts:200-209, http.ts:46-50, control.ts:421-426.


12. --owners setup

sequenceDiagram
  participant P as Person
  participant K as Control
  participant C as Core
  participant O as Owner Alice
  participant B as Member Bob
  P->>K: serve my-agent --owners
  K->>C: /_create link #G observer
  K->>P: The link adds an observer. Its ID and name are printed when it joins.
  O->>C: joins by the link
  C->>K: joinedGroupMember
  K->>C: /_delete link #G
  K->>P: Alice joined (member 3). Restart the server with --owners 3:Alice
  K->>O: Alice joined (member 3). Restart the server with `--owners 3:Alice`
  P->>K: Ctrl-C, then serve my-agent --owners 3:Alice
  K->>C: /_member role #G 3 owner
  K->>C: /_remove #G ID, for each other human
  K->>C: /_delete link #G
  K->>P: Owners: 3:Alice.
  K->>P: Add the agent to Claude Code: ...
  O->>C: adds Bob, makes Bob an owner
  C->>K: memberRole
  K->>P: Bob (member 7) is now an owner and can't approve actions yet. Restart ... --owners 3:Alice,7:Bob
  K->>O: same line, the --owners part as code
  B->>K: /approve N
  K->>B: Only the owners listed in --owners approve actions.

Notes:

  • Until the restart with a list, new actions are refused: The agent has no approving owner. ... (control.ts:108-110).
  • An entry must match a human by member ID and display name, folded to one line as the restart line prints it, or the server exits with code 2 (control.ts:249-253).
  • With --reset-owners, the server deletes the control chat for all members and creates a new one with the same counter; the CLI refuses the option with an --owners list (control.ts:387-392, cli.ts:97-99).
  • When a listed owner leaves and none remains: ... is no longer an owner. No owners remain: restart the server with --reset-owners --owners (control.ts:288-293).

Source: control.ts:242-264, :275-294, :357-361, :565-591.