Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
25 KiB
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
- First start and the owner joins
registerconnectwith approvalsendwith responsesreceivewithwaitsendunder a standing approval, and/revokeleave- Mode
host - Expiry
- A lost call and its re-attachment
- Shutdown with pending actions
--ownerssetup
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 lineIf the QR code does not scan, ...is printed after the link (control.ts:307). - The server runs
ownerJoinedfor everyjoinedGroupMemberevent 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-mcpignores the exit status ofclaude mcp removeand stops at that step only when theclaudebinary is missing (register.ts:35-36).timeoutis(expireHours + 24) × 3600000 + 600000ms fromserver.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:
- The plan is read again in the perform step (tools.ts:484).
- Join commands per plan (tools.ts:514-542):
| 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
contactConnectedoruserJoinedGroup: 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 untilcreated + 23 h 50 min, and then reportsconnected: false(tools.ts:483, :507-511). - Without
message, the idle time is counted from the connection; withmessage, from the first reply (tools.ts:568, messages.ts:225). - For a target the agent is connected to, the result is
status: knownwith its chat fields, and no action is created (tools.ts:417). - Exact progress texts:
Action N waits for an approver until TIME., thenAction N waits for the connection until TIME., thenK 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
observerorrelayrefused,reply_toitem 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
responsesand counted inmore(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 ofafter=<last> count=100until it holdslimit + 1unread, 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:
- The approval is used in mode
requestswhile memberapprovedByIdis an approver (control.ts:113-116). - For sends that were pending at
/approve CHAT all, the card ends with the plainApproved by Alice.(control.ts:199-201, :419). connectandleaveactions are created without the standing approver, so they stay pending (tools.ts:423, :638, commands.test.ts:360-369).
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
fullwithoutnotify=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,undefinedis returned bystandingApprover, so standing approvals are ignored bysend,listand/list(control.ts:115). _metais set onconnect,sendandleaveonly (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
/pendingreply (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:
- The key is the tool name with the action fields:
target,incognito,messageforconnect;chat,text,reply_to,mentionforsend;chatforleave(tools.ts:413, :582, :633, :1003-1005). - The
wait,idleandlimitof the attaching call are used (tools.ts:593). - For a call aborted before
create, the server returns before it creates an action (tools.ts:422, :585, :637). - Abort path in the SDK: on
resclose the web request is aborted (node index.cjs:299-303); the per-request transport is closed (server index.cjs:86-90); the handler signal is aborted (src-BhnGYybN.cjs:6425).
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
pendingand 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;
/approveof their numbers gets the replyAction N is not pending.(control.ts:182). - The stop signal of the performs is aborted after the actions are marked interrupted: a
connectwaiting for its connection ends, and an interrupted perform logs no line (actions.ts:218, :252, tools.ts:507-509). - An error of a
/sqlquery of the delivery wait is logged asdelivery wait: ERRORon 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--ownerslist (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.