* bots, nodejs: document chat, connection, member contact and network commands, typed commands in library * bots, nodejs: restore apiConnect, wrap domain once, test all connect responses * nodejs: JSDoc for new methods * docs: plan for bot API commands and typed Node library methods * tests: channel prepared with a domain has a verified SimpleX name * docs: apply review fixes to the plan for bot API commands * docs: apply second review pass to the plan for bot API commands * docs: refer the bot API plan to the rewritten test commit * docs: record decisions in the plan for bot API commands * docs: plan tests of the generated Python command strings * python: test the generated strings of the documented commands * docs: plan for files, /leave and library methods in the MCP server * docs: wait at stop in the MCP plan * docs: library methods and /leave mechanics in the MCP plan * mcp: typed library methods for chat commands * docs: plan the message information command for bots * mcp: /leave CHAT owner command to leave a chat at once * docs: file servers, GIF and video choices decided in the MCP plan * docs: file mechanics in the MCP plan * mcp: files of received messages * bots, nodejs: message information with the file servers for bots * docs: plan the relay approval option of receiving files in Node * mcp: download of received files with owner approval * docs: name the xftp-server test helper in the files plan * nodejs: receive files without approving unknown relays * docs: plan the user servers command for bots * bots, nodejs: user servers and relays by operator for bots * nodejs: document the message information method * docs: plan the file upload progress event for bots * mcp: send files with owner approval, refusing private keys * bots: file upload progress event for bots * mcp: /leave template in the agent control chat menu * docs: sent copies are deleted for contacts too, at sndFileCompleteXFTP * bots: drop the file upload progress event * docs: drop the upload progress event plan * nodejs: document user servers and relay approval of received files * mcp: download cards name unknown file servers after the upload * docs: record the wait for the file description in the files plan * mcp: fixed wait at stop for cancelled cards, without /sql * docs: record decision C4, a fixed wait at stop, in the files plan * ci: test the MCP server package on pull requests * ci: state when the MCP workflow can be dispatched * ci: run the MCP server tests when its libraries change * mcp: log the errors of image previews and resizing * mcp: keep files in transfer in the data folder * mcp: restrict sent files by the real paths of refused folders * mcp: validate the file at the send by its inode and change time * mcp: limit ffmpeg and ffprobe to local files and no input commands * mcp: ask the owner for unknown file servers; read approvals at creation * mcp: name downloaded files by their ID; return paths of downloads only * mcp: end each download with a result for the agent and the card * mcp: show files above their sender's size limit as failed * mcp: refuse empty files, as without file and captions too long to send * mcp: tell the owner about failed uploads and unposted received files * mcp: delete resized copies left by the last run at start * mcp: name downloads and leaving in the help and greeting, align docs * mcp: compute the download path for received files only * mcp: send a /receive file to the owner under its name * mcp: keep files received without a download in the chat's folder * mcp: keep a left chat's files and delete them with /delete CHAT files * mcp: test that sent images are resized by the core * mcp: wait 30 minutes for a download's progress before cancelling it * mcp: end a download as deleted only when its message is not found * mcp: delete the copy of a /receive file whose post fails * mcp: count the quoted content in a message's length, as the core sends it * mcp: name received bytes, unknown-server downloads and left chat files plainly * mcp: test refused folders, file changes and download approvals * docs: align MCP server docs with refused folders, leaves and download waits * mcp: move received files only in full, delete partials only of downloads * mcp: report a received file missing from its folder as failed * mcp: move received files of a group's member chats into its folder * mcp: read a video's preview and duration as an MP4 or QuickTime file * mcp: 0.2.0 on simplex-chat 7.1.0-beta.10.1 * mcp: file folders by chat type, /delete cancels downloads, files reply * docs: MCP server specs and plan for 0.2.0 * mcp: /receive gives the agent a file, /forward sends it to the owner * docs: MCP server specs for /receive and /forward of download cards * docs: MCP plan records the temp folder command as a library exception * mcp: /delete cancels resumed downloads; /forward quote fits the limit * docs: MCP specs and plans for /forward quotes and resumed downloads * docs: MCP specs name the source commit they are checked against * docs: MCP PLAN states the /forward quote and move rules * mcp: a download moves its completed file whatever its size * mcp: /receive N all covers its card and later files, not pending ones * docs: MCP files approval scope, reply text and README server wording * mcp: a download approves relays only for a card naming unknown servers * mcp: files in member chats of a group are tracked as in the main chat * docs: MCP relay approval of downloads and files in member chats * mcp: a rejected file gets a card again under the files approval * docs: MCP rejected files under the files approval, README scope * mcp: an offered file without a description gets no card or action * docs: MCP a file without a description is not downloaded * mcp: 0.2.0 on simplex-chat 7.1.0-beta.10.2 * ci: test the MCP server as a scope of build-linux
57 KiB
SimpleX MCP server: architecture
System structure, modules, state, process lifecycle, approval model and security boundaries of
@simplex-chat/mcp0.2.0.Related specs: README | Flows | API Related docs: README | Reference | PLAN
Source: cli.ts | serve.ts | http.ts | tools.ts | actions.ts | control.ts | messages.ts | files.ts | register.ts | log.ts
Table of contents
1. System diagram
flowchart LR
subgraph host["MCP host"]
cc["Claude Code"]
end
subgraph proc["Node.js process: simplex-mcp serve NAME"]
http["http.ts<br/>127.0.0.1:PORT/mcp<br/>Host and Origin guards<br/>bearer token<br/>path /mcp"]
sdk["MCP SDK<br/>createMcpHandler<br/>McpServer per request"]
tools["tools.ts<br/>AgentTools"]
actions["actions.ts<br/>Actions"]
control["control.ts<br/>Control"]
messages["messages.ts<br/>ChatItemsWatch"]
serve["serve.ts<br/>start, events, stop"]
files["files.ts"]
log["log.ts"]
lib["simplex-chat<br/>ChatApi"]
core["SimpleX core<br/>libsimplex core-7.1.0.20"]
end
cli["cli.ts"] --> serve
cli --> register["register.ts"]
register -->|"claude mcp add-json"| cc
cc -->|"headersHelper:<br/>cat headers.json"| data
cc -->|"POST /mcp<br/>Authorization: Bearer"| http
http --> sdk --> tools
tools --> actions
actions <--> control
tools --> messages
serve --> control
serve --> tools
serve --> messages
serve --> files
files --> data[("DATA:<br/>token<br/>headers.json<br/>server.json<br/>lock")]
tools --> folder[("DATA/files:<br/>group-N, contact-N<br/>sent")]
tools --> lib
control --> lib
serve --> lib
lib --> core
core --> db[("DATA:<br/>simplex_chat.db<br/>simplex_agent.db<br/>tmp")]
core <-->|"SMP<br/>private routing"| smp["SMP servers"]
core <-->|"XFTP"| xftp["XFTP servers"]
smp <--> owner["Owner's SimpleX app<br/>agent control chat"]
smp <--> peers["Peers:<br/>contacts and bots<br/>groups and channels"]
log --> term["Terminal:<br/>stdout<br/>stderr"]
Data flow of a tool call:
- Claude Code runs the
headersHelpercommand of its entry (register.ts:50) and sends theAuthorizationheader it prints (Reference). - The server checks Host, Origin, token, path and readiness, then passes the request to the SDK handler (
http.ts:19-28). - An
McpServeris built from the factory for each request, and the arguments are validated against the input schema by the SDK (serve.ts:111-115,mcp-BumlNux0.cjs:1812-1817). - The handler result is returned by
AgentTools.callasstructuredContentand as JSON text (tools.ts:500-507). - The server calls
ChatApimethods of the core (messages.ts:267-283); actions are created inActionsand shown to the owner byControl(actions.ts:155-186). - The core emits events; the server dispatches them in one handler of
serve.tsto the tools, the item watch and the agent control chat (serve.ts:129-165).
2. Modules
2.1 cli.ts
Entry point of the simplex-mcp binary.
| Symbol | Line | Contents |
|---|---|---|
helpText |
10 | --help output |
Command |
40 | help, serve with ServeOptions, register with RegisterOptions |
parseCommand |
45 | command, NAME and options to a Command |
serveOptions |
54 | defaults and option switch of serve |
registerOptions |
110 | defaults and option switch of register |
OptionReader |
127 | value: the next argument, requiredoptionalValue: the next argument, absent when missing or starting with -- |
validAgentName |
149 | ^[a-z0-9](-?[a-z0-9])*-?$, at most 32 characters |
usageError |
194 | ExitError with code 2 |
main |
198 | help to stdout, serve, register |
| top-level catch | 211-220 | ExitError: message on stderr, its code; other errors: logError, code 1 |
2.2 serve.ts
Start sequence, event dispatch and stop sequence of one agent.
| Symbol | Line | Contents |
|---|---|---|
version |
15 | the package version |
OwnerEntry |
19 | {memberId, name} of --owners |
ServeOptions |
24 | name, data folder and its dataGiven flag, display name, mode, owners, expiry, reset and token flags, port, QR code size, stop wait |
RunningServer |
39 | token, ChatApi, user, Actions, stop |
serve |
47 | startServer, then SIGINT and SIGTERM handlers: the first signal stops, a second ends the stop wait |
startServer |
65 | data folder, lock, token, headers file, ChatApi.init, startAgent; cleanup on error |
startAgent |
88 | user, network config, temp folder, control group, wiring of Actions, AgentTools, Control, HTTP, event handler, stop |
registerHint |
185 | the register command line printed after the owner setup |
waitForCancelledCards |
189 | the fixed wait at stop for cards edited or posted at the stop |
setTempFolder |
205 | DATA/tmp, mode 0700, as the core's temp folder |
updatedUser |
211 | display name and peer type agent of the profile |
controlGroup |
218 | found, created or reset agent control chat |
listen |
225 | startHttp; EADDRINUSE to ExitError code 1 |
bufferEvents |
234 | events received before the handler is installed are queued and replayed in order |
2.3 http.ts
| Symbol | Line | Contents |
|---|---|---|
HttpOptions |
7 | port, token, McpServerFactory, readiness |
startHttp |
14 | http.Server on 127.0.0.1:port: guards, SDK handler, timeouts |
authorized |
34 | length check and timingSafeEqual against Bearer <token> |
stopHttp |
48 | server.close and closeAllConnections |
2.4 tools.ts
Tool definitions, call handlers, connect plans, perform steps, files.
| Symbol | Line | Contents |
|---|---|---|
ConnectInput, SendInput, ReceiveInput, LeaveInput, DownloadInput |
46-80 | tool inputs |
PlanSummary |
86 | card kind, chat type, name, SimpleX name, peer type, description, welcome, commands, takesMessage |
ChatFields |
98 | chat fields of connect and list results |
Agent |
112 | ChatApi, user, Actions, ChatItemsWatch, control chat reference, standing approver lookup, refused file post, data folder |
JoinMethod |
123 | prepared, prepareChannel, viaAddress, prepareContact, connect |
| schemas | 178-401 | input and output JSON schemas |
descriptions, instructions |
403-441 | tool descriptions and server instructions |
definitions |
451 | name, description, schemas and action flag of the six tools |
Download |
462 | one download: its promise, chat, item, file ID, size, path, the last fileSource seen, received bytes and their time, check timer |
AgentTools.register |
488 | registerTool for each definition; _meta in mode host |
AgentTools.call |
500 | handler result to CallToolResult; errors to isError |
AgentTools.perform |
509 | perform step of an approved action |
downloadFile, forwardFile, downloadTo |
522-549 | one download per file ID, shared by download and /forward; a forwarded file moved to the download path for download |
| file events | 551-588 | onNewItems, onFile, onFileComplete, onFileError, onFileCancelled, onFileDescription |
onContactConnected, onUserJoinedGroup |
590-598 | completion of connect actions by events |
actionResult |
636 | wait for the decision, result fields, removal |
attachment, isRefused, fileToSend, resizedCopy |
860-896 | the file of a send |
copyForSending, chatFiles, deleteChatFiles |
898-927 | /forward copies; the files of a chat, forwarded ones included; its downloads cancelled, also those the core continues after a restart |
leaveChat |
950 | perform step of leave and /leave; the chat's forwarded files deleted |
download, fileServers, unknownServers |
960-1006 | the download call before its action |
startDownload, checkDownload, currentFile, settleFile, settleReceived, moveReceived |
1024-1091 | a download's steps |
agentChats, knownChat |
1160-1177 | the agent's chats; a checked chat reference |
classifyPlan |
1260 | apiConnectPlan response to a join plan or a known chat |
chatSubject, isConnected, isActive, memberCurrent |
1356-1409 | chat and member predicates shared with control.ts |
fitsMessage, quoteContent |
1433-1449 | the message as the core sends it, within 15602 bytes; for send and the post of /forward |
realFile |
1465 | real path, size, inode and change time of a file |
attachmentMedia, videoFrame, videoDuration |
1512-1546 | previews and durations |
toolCall |
1562 | ServerContext to ToolCall: abort signal and progress notifications |
2.5 actions.ts
| Symbol | Line | Contents |
|---|---|---|
Mode |
5 | requests, host |
State |
6 | pending, performing, done, failed, rejected, expired |
Standing |
7 | messages, files |
ToolError |
19 | errors the model can correct; text returned as is |
Subject |
21 | kind, name, SimpleX name, incognito flag of a card |
ConnRef |
28 | pending connection, contact or group of a connect action |
ActionRecord |
34 | number, key, times, change signal, standing flag, state, card, decider, open calls, interrupted flag, error, card image |
ConnectAction, SendAction, LeaveAction, DownloadAction |
51-110 | per-tool fields; Attachment, the file of a send |
ActionHooks |
116 | refusal, saveCounter, postCard, perform, outcome |
Actions |
124 | action map, expiry timer, posting and outcome promises |
create |
155 | refusal checks, number, log line, counter write, card |
decided |
206 | wait with progress until a decided state, until, or the abort signal |
stop |
240 | cancellation of pending and performing actions |
summary, performedText, progressText |
315-354 | log and progress texts |
Hooks are bound in serve.ts:100-106: refusal, saveCounter, postCard and outcome to Control; perform to AgentTools.
2.6 control.ts
| Symbol | Line | Contents |
|---|---|---|
controlCommands |
10 | the command menu |
standings |
22 | custom data key and reply words of the approvals of messages and files |
ControlData |
43 | {control: true, nextAction} |
ChatApproval |
60 | {approvedBy, approvedById, approvedAt} |
OwnerRequest |
66 | parsed owner command |
Control.load |
102 | lost check, group profile and menu, member list |
Control.start |
111 | owner setup per --owners |
Control.refusal |
119 | bytes left for the link texts of a connect card, card size of a send, connect or download, approver presence |
Control.standingApprover |
130 | approver name of a chat's standing approval of messages or files |
Control.saveCounter |
135 | queued counter writes |
Control.postCard, Control.outcome |
141-152 | card post and card edit; the kept-files message after a done leave |
Control.refusedFile, Control.fileNotSent |
154-169 | posts about files |
Control.onMessage |
171 | owner commands, queued one at a time |
Control.onEvent |
183 | membership changes and loss of the control chat |
Control.ownerCommand |
200 | command dispatch and replies |
approveChat, revoke, forward, leave, deleteFiles |
243-307 | owner requests |
cardContent, fittedImage |
441-461 | image and video cards with the file's preview |
findControlGroup, createControlChat, resetControlChat |
499-527 | control group lookup, creation, reset |
isControlEvent |
529 | membership events of the control group |
parseCommand |
545 | one-line /keyword params |
cardText, requestLines, commandsLine |
552-616 | card text per tool, state and mode |
largestCard, encodedLength |
622-630 | card size check |
serversLine, sizeText |
664-673 | servers line and sizes of download cards |
codeSpan, safeName, mentionText |
683-695 | quoting of peer text |
ownerRequest |
710 | command words to an OwnerRequest |
qrCodeText |
725 | terminal QR code |
losesControl, isLost |
741-756 | loss of the control chat |
entryText, restartText |
774-784 | --owners entries in restart lines |
2.7 messages.ts
| Symbol | Line | Contents |
|---|---|---|
Message, MessageFile, FileOffer |
16-50 | message item of tool results; its file; the file of a download |
ToolCall |
57 | abort signal and progress callback of one call |
Collected |
62 | collected items, change signal, IDs of the items over the first 100, time of the last change |
Changes |
69 | set of wake callbacks |
ChatItemsWatch |
86 | listeners per chat reference; received message items gathered by collect |
chatRef, anyScopeChatRef, isChatRef |
135-152 | @N and #N references: of the main conversation, or also of a group's member chat |
toMessage, messageFile, receivedFileError, receivedPath |
174-213 | chat item to Message; where a received file is: its download path or its forwarded path |
filePaths, chatFolder |
215-223 | DATA/files/group-N, DATA/files/contact-N or the same under DATA/forwarded; the download path and the forwarded path of a received file |
fileOffer, fileContent |
225-250 | file fields of an item; message content of a file |
getChat, receivingFiles, markRead |
267-283 | apiGetChat and apiChatItemsRead by chat reference; the IDs of a chat's files in status rcvAccepted, rcvTransfer or rcvWarning, paged |
unreadItems |
285 | unread items of a chat after its initial read, paged |
reportProgress, nextChange, awaitResponses |
301-331 | waits |
validName, viewName |
337-363 | display name rules and @name form |
2.8 files.ts
| Symbol | Line | Contents |
|---|---|---|
createDataFolder |
12 | mkdir -p mode 0700; chmod 0700 except on Windows |
acquireLock, releaseLock |
17-28 | DATA/lock with the PID |
serverToken |
30 | DATA/token: read, or 32 random bytes in base64url |
writeHeadersFile |
42 | DATA/headers.json |
writeServerFile, readServerFile |
46-53 | DATA/server.json |
2.9 register.ts
| Symbol | Line | Contents |
|---|---|---|
register |
33 | claude mcp remove, then claude mcp add-json |
registryEntry |
46 | {type, url, headersHelper, timeout} |
manualEntryText |
55 | entry text when claude is missing |
projectRoot |
60 | project key of --scope local |
runCommand |
86 | spawnSync with ENOENT detection; on Windows after ENOENT, once more through cmd.exe with cmdArgument quoting, and exit status 9009 as missing |
2.10 log.ts
| Symbol | Line | Contents |
|---|---|---|
ExitError |
18 | message and exit code |
shellWord, shellPath |
24-31 | POSIX single quotes or Windows double quotes |
oneLine |
33 | runs of \p{Cc}, \p{Cf}, \p{Zl}, \p{Zp} to one space |
log, logError |
37-43 | stdout lines; stderr lines |
errorText, chatErrorText |
45-53 | error text, folded to one line: the apps' wording for known error shapes, otherwise the type chain |
appErrorText |
55-64 | wording of the apps' alerts for chat, agent and store errors |
fileError |
197-201 | the error of a received file: the apps' words for its status, or the size limit of its sender |
serverHostname |
246 | host of a server address |
chainText, errorChain |
250-262 | SimpleX error: <type chain>: <scalar fields>, details at most 500 characters |
fullDomainName |
272 | SimplexDomain to name.simplex or name.testing |
3. State
3.1 Memory
| Item | Holder | Written by | Read by |
|---|---|---|---|
| Actions | Actions.actions map (actions.ts:128) |
create, remove, expireDue |
tools, Control |
| Open calls of an action | ActionRecord.open (:44) |
actionResult (tools.ts:637, :646) |
list, expireDue |
| Collected replies | ConnectAction.messages, SendAction.responses (:56, :87) |
ChatItemsWatch.collect |
connect, send results |
| Item listeners | ChatItemsWatch.listeners (messages.ts:87) |
collect |
event dispatch |
| Downloads | AgentTools.downloads, one per file ID (tools.ts:480) |
downloadTo |
file events, checks, /delete CHAT files |
| Forwards | AgentTools.forwarding, the copy for the post per file ID (tools.ts:481) |
forwardFile |
downloadFile, before it moves a forwarded file |
| Rejected files | AgentTools.rejectedFiles (tools.ts:482) |
downloadResult (:1013-1014) |
the file field; download, which then skips the files approval (:975) |
| Count of file descriptions | AgentTools.describedFiles (tools.ts:483-484) |
onFileDescription |
fileServers |
| Members of the control chat | Control.members (control.ts:82) |
refreshMembers (:463-465) |
approver checks |
| Mode, expiry, owners list, stop wait | ServeOptions of the run |
CLI | all modules |
3.2 Chat database custom data
| Item | Entity | Value | Written by | Read by |
|---|---|---|---|---|
| Control marker and counter | agent control chat | {simplexMcp: {control: true, nextAction: N}} |
createControlChat, saveCounter |
findControlGroup, controlDataOf |
| Standing approval of messages | contact or group of the agent | {...other keys, simplexMcp: {approvedBy, approvedById, approvedAt}} |
approveChat, revoke |
standingApprover |
| Files approval | contact or group of the agent | {...other keys, simplexMcpFiles: {approvedBy, approvedById, approvedAt}} |
approveChat, revoke |
standingApprover |
The profile, chats, items, members and file transfers are stored by the core in DATA/simplex_chat.db; connections and queues in DATA/simplex_agent.db (file prefix DATA/simplex, serve.ts:72).
3.3 Files
| Path | Contents | Mode | Written | Read |
|---|---|---|---|---|
DATA/ |
data folder | 0700, set at every start except on Windows | files.ts:12-15 | |
DATA/token |
base64url of 32 random bytes | 0600, set again at every write except on Windows | first start, --new-token (:30-36) |
every start |
DATA/headers.json |
{"Authorization":"Bearer <token>"} |
0600, set again at every write except on Windows | every start (:42-44) | Claude Code headersHelper |
DATA/server.json |
{name, port, expireHours} |
0600, set again at every write except on Windows | every start, after the HTTP listener is bound (serve.ts:118) | register |
DATA/lock |
PID and a newline | 0600 | start (files.ts:64-72) | next start |
DATA/tmp/ |
the core's temp folder: files in transfer | 0700, set at every start | serve.ts:93, :205-209 | the core |
DATA/files/group-N/, DATA/files/contact-N/ |
downloaded files and files received without a download, named by file ID and extension | folder 0700 | the core at the download path (tools.ts:1029-1030); moves (:1500-1510) | download results, /forward, /leave, /delete CHAT files |
DATA/forwarded/group-N/, DATA/forwarded/contact-N/ |
files /forward downloaded, named as above, until a download moves them to DATA/files/ |
folder 0700 | the core at the forwarded path (tools.ts:532-539, :1029-1030) | /forward; moved by download (:522-530); deleted by leave, /leave and /delete CHAT files (:909-927, :950-958) |
DATA/files/sent/ |
resized copies of sent images, and copies of the files /forward posts, one folder per copy |
folder 0700; a resized copy 0600 | tools.ts:885-902, :929-933 | the core; deleted at sndFileCompleteXFTP, sndFileError and at start (serve.ts:94, :156-160) |
The core library is downloaded once into CACHE/core-7.1.0.20/sqlite, and the Node.js add-on into CACHE/nodejs-addon-cce06d8/, each with a Downloading ... line on stderr. CACHE is $XDG_CACHE_HOME/simplex-chat or ~/.cache/simplex-chat; on macOS ~/Library/Caches/simplex-chat; on Windows %LOCALAPPDATA%\simplex-chat (libs.ts:41-48, :89, :109, :116, :129).
4. Process lifecycle
4.1 Start
- Data folder, lock, token and headers file are prepared (serve.ts:67-71).
- libsimplex for SQLite is loaded by
ChatApi.init, and the databases are migrated with confirmationyesUp(serve.ts:72, api.ts:117-126). - Three log lines: version and NAME, data folder, mode (serve.ts:73-75).
- The active user is read, or created with display name
--display-nameor NAME, empty full name and peer typeagent(serve.ts:90). - The network config is read and set with
smpProxyModealwaysandsmpProxyFallbackallowProtected(:91-92). DATA/tmpis set as the core's temp folder, andDATA/files/sentis deleted (:93-94).- Events are buffered, and the chat is started (:95-96).
- A
--display-namedifferent from the profile, or a peer type other thanagent, is applied with oneapiUpdateProfile(:97, :211-216). - The agent control chat is found by its marker, created with counter 1, or reset with
--reset-owners(:99, :218-223). Actions,ChatItemsWatch,AgentToolsandControlare created; the counter is taken from the marker (:100-109).Control.load: for a lost control chat,Actions.lostis set; otherwise the group name and command menu are updated when they differ, and members are read (control.ts:102-109).- The HTTP server listens; with the port in use, the process exits with code 1 (serve.ts:117, :225-232). Until step 14 ends, requests to
/mcpthat pass the token check get 503 withRetry-After: 1(http.ts:23, serve.ts:116-117, :125). server.jsonis written (serve.ts:118).- The server runs the owner setup,
Control.start; after an error, HTTP is closed and the error is rethrown (serve.ts:119-124). Listening on http://127.0.0.1:PORT/mcp(:126).- Buffered events are replayed, then live events are dispatched (:129, :234-248).
After an error inside startAgent (steps 4-16) the chat is closed; after any error once the lock is taken, the lock is released (serve.ts:69-85).
4.2 Event dispatch
Each onAny receiver is awaited by the library before the next event is read (api.ts:195-202). The dispatcher in serve.ts:129-165:
| Event | Condition | Handler |
|---|---|---|
newChatItems |
item of the control group, main scope | Control.onMessage |
newChatItems |
every item | AgentTools.onNewItems, then ChatItemsWatch.deliver |
chatItemUpdated |
AgentTools.onFile, then ChatItemsWatch.update |
|
chatItemsDeleted |
ChatItemsWatch.remove of each deletedChatItem |
|
contactConnected |
AgentTools.onContactConnected |
|
userJoinedGroup |
AgentTools.onUserJoinedGroup |
|
rcvFileComplete |
AgentTools.onFileComplete |
|
rcvFileSndCancelled |
AgentTools.onFileCancelled |
|
rcvFileError |
AgentTools.onFileError |
|
rcvFileDescrReady |
AgentTools.onFileDescription |
|
sndFileCompleteXFTP |
AgentTools.onSentFile |
|
sndFileError |
AgentTools.onSentFile, then Control.fileNotSent |
|
joinedGroupMemberjoinedGroupMemberConnectingconnectedToGroupMembermemberRoleleftMemberdeletedMemberdeletedMemberUsergroupDeleted |
event of the control group | Control.onEvent, queued one at a time |
| any event | after stop() started |
ignored |
Owner commands are queued in a second chain, independent of the membership chain (control.ts:177-180).
4.3 Stop
On SIGINT or SIGTERM the server runs one stop; a later signal ends its wait for cancelled cards (serve.ts:47-63). RunningServer.stop (serve.ts:171-181):
- Events are ignored from now on.
Actions.stopstarts: new actions are refused withThe server is stopping.; the expiry timer is cleared; cards being posted are awaited; pending and performing actions are marked interrupted; the stop signal of the performs is aborted, which ends the wait of aconnectfor its connection; their cards are edited, or posted when they have no card; outcome edits already in progress are awaited (actions.ts:240-250).- In parallel with step 2, the HTTP server is closed with all connections, and open calls are aborted (http.ts:48-52).
Cancelled K action(s).when K > 0 (serve.ts:176).- When cards were edited or posted at the stop and the control chat is not lost, the server waits
--stop-waitseconds, 10 by default, for them to be sent; a second signal ends the wait (serve.ts:177, :189-203). chat.close(): the chat is stopped, then the store is closed (api.ts:165-170).- The lock is removed,
Stopped.is logged, and the process exits with code 0, or 1 after an error (serve.ts:179-180, :53-59).
Performs in progress are not awaited; when one ends, its card stays as edited at the stop and no line is logged, because the action is interrupted (actions.ts:280).
4.4 The lock
acquireLock(DATA): files.ts:17
create DATA/lock exclusively (flag wx), PID :64-72
exists:
PID = number in the file :74-77
PID valid, other than ours, running: :21, :79-86
exit 1 "Another server uses DATA (PID N). If no server runs, remove DATA/lock."
remove the file; create it exclusively again :22-23
exists again: exit 1 with the same message
releaseLock(DATA): remove DATA/lock :26-28
isRunning: true when kill(pid, 0) succeeds or fails with EPERM (files.ts:79-86).
5. Approval model
5.1 Modes
The mode is set by --approve for one run (cli.ts:76-78).
| Mode | State of a new action | Card | _meta of connect, send, download, leave |
|---|---|---|---|
requests |
pendingperforming for a send under a standing approval, and for a download under a files approval |
card with commands outcome as a new message after a standing send; none after a download without a card |
none |
host |
performing |
outcome as a new message after the perform; none for a download | {"anthropic/requiresUserInteraction": true} |
Sources: actions.ts:166, tools.ts:494, control.ts:145-152, :495-497. In both modes a new action is refused unless the control chat has an approver (control.ts:124-127).
5.2 Owners and approvers
| Term | Rule | Code |
|---|---|---|
| human | member other than the agent's membership, status connected or complete |
control.ts:484-486, tools.ts:1394-1396 |
| human owner | human with role owner |
control.ts:471-473 |
approver, --owners absent |
member with role owner |
control.ts:488-492 |
approver, --owners without a list |
none | control.ts:491 |
approver, --owners ID:NAME,... |
member with role owner whose groupMemberId and displayName, folded to one line by oneLine, match an entry |
control.ts:491, :758-760 |
The command sender's member record from the chat item is checked (control.ts:176, :204). The approver count for refusals and standing approvals is taken from Control.members, refreshed at load and at each membership event (control.ts:187, :463-465).
Owner setup at start (control.ts:111-117):
--owners |
Start | First join by the link |
|---|---|---|
| absent | a human owner exists: register hint otherwise: No owners in the agent control chat. when humans exist, then a link with role member |
link deleted, role owner, greeting, log line, register hint |
| without a list | no humans: link with role observerhumans exist: their IDs and the restart command, in the log and the chat |
link deleted, the joined member and the restart command announced |
ID:NAME,... |
each entry matched to a human, or exit 2 role owner set where missingother humans removed link deleted Owners: ... and register hint |
Sources: startOwners, startObserver, startListed, ownerJoined, observerJoined.
Membership changes at run time (control.ts:183-198):
| Change | --owners |
Server reaction |
|---|---|---|
deletedMemberUser, groupDeleted, or the agent's role below owner |
any | Actions.loseControl: pending actions set to failed; later cards left unedited; log line |
| approvers drop to zero after a human owner existed | absent | No owners in the agent control chat., new link with role member |
| a human becomes owner outside the list | list | <name> (member N) is now an owner and can't approve actions yet. Restart the server with --owners ... |
| a listed owner stops being a human owner | list | <name> (member N) is no longer an owner. with the restart command, or No owners remain: ... |
5.3 Action state machine
Transitions, one per row:
| Event | From | To | Code |
|---|---|---|---|
call creates the action, mode requests |
pending |
actions.ts:166 | |
call creates the action, mode host |
performing |
actions.ts:166 | |
send call under a standing approval, or download call under a files approval |
performing |
actions.ts:166 | |
| counter write or card post fails | pendingperforming |
failed |
actions.ts:252-263 |
stop arrives while the counter of a performing action is written |
performing |
pending, interrupted |
actions.ts:265-270 |
| control chat lost | pending |
failed |
actions.ts:232-238 |
/reject, or /leave CHAT for a pending action of CHAT with a card |
pending |
rejected |
actions.ts:225-230, control.ts:291-293 |
| expiry time passes | pending |
expired |
actions.ts:196-199 |
/approve N, /approve CHAT N and /approve CHAT all for other actions; /receive N, /receive CHAT N and /receive CHAT N all for downloads |
pending |
performing |
actions.ts:219-223, control.ts:229-238 |
| perform throws | performing |
failed |
actions.ts:276-279 |
| perform ends | performing |
done |
actions.ts:274-275 |
create (actions.ts:155-186):
create(key, fields, standingApprover?):
state = mode host or standing ? performing : pending
refusal, first match:
stopping "The server is stopping."
control chat lost lostMessage
20 actions pending pendingMessage
20 actions performing performingMessage
connect: link texts cut to the bytes its largest card leaves
send, connect or download card over 15000 bytes cardTooLong
no approver no-owner message
refusal: throw ToolError counter unchanged
N = next; next += 1; store; log "Action N: ..."
post:
saveCounter(next)
state pending: card = postCard()
either fails: state failed; log "Action N: failed: ERROR."
after post:
posted and performing:
stopping: state = pending
otherwise: perform
stateDiagram-v2
[*] --> pending: mode requests
[*] --> performing: mode host, or standing
pending --> performing: /approve, /receive
pending --> rejected: /reject, /leave CHAT
pending --> expired: expiry
pending --> failed: counter or card fails, control chat lost
performing --> done: perform ends
performing --> failed: perform throws, counter fails
performing --> pending: stop during the counter write
An action is removed from memory when its result is returned to a call (tools.ts:643), or at an expiry tick when it is decided, its open call count is 0 and its expiry time has passed (actions.ts:200-201). A download still performing at the end of wait is returned as performing and kept (tools.ts:640).
5.4 Cards
The server posts a card when a pending action is created, and edits it with apiUpdateChatItem at the outcome (actions.ts:255, control.ts:145-152). For a connect, send or leave without a card (mode host, standing sends) the server posts the outcome as a new message; a download without a card gets none (control.ts:495-497). The text per state is built by cardText; a card of an image or video file is an image or video message with the file's preview (control.ts:441-461); forms are listed in API: cards.
Outcomes are posted after done and failed (actions.ts:282), after rejected and expired (:229, :199), and at stop (:247). They are skipped after the control chat is lost (:247, :282, :291).
5.5 Standing approvals
| Aspect | Messages | Files | Code |
|---|---|---|---|
| grant | /approve CHAT all from an approver in mode requests; pending sends of CHAT with a card are approved |
/receive CHAT N all from an approver, on the card of download N; N is performed, and the other pending downloads of CHAT stay pending |
control.ts:237, :243-254 |
mode host |
nothing stored; reply Approvals apply in mode requests; this server runs in mode host. |
downloads get no card, so the command has none to act on | control.ts:213 |
| removal | /revoke CHAT: the simplexMcp key is removed, other keys kept |
/receive CHAT off: the simplexMcpFiles key is removed |
control.ts:256-263 |
| use | mode requests, and approvedById equal to the groupMemberId of a current approver |
the same | control.ts:130-133 |
| scope | send |
download of a file whose servers are all known |
tools.ts:796, :975 |
| decider shown | safeName(approvedBy) |
the same | control.ts:132 |
| storage | the chat's custom data; removed with the chat by leave |
the same | tools.ts:950-958 |
5.6 Expiry
| Rule | Code |
|---|---|
expires = created + expireHours × 3600000, expireHours 1 to 23, default 23 |
actions.ts:163, cli.ts:59, :82-84 |
| check every 60 s on an unreferenced timer, and before each owner command | actions.ts:129, control.ts:201 |
pending and due: expired, log Action N expired., card edited to Expired. |
actions.ts:196-199 |
| decided, no open call, due: removed from memory | actions.ts:200-201 |
connect wait for the other side: at most until created + 24 h - 10 min |
tools.ts:143, :689 |
Date.now() is compared with expires, so due actions are expired at the first tick after a sleep of the computer.
5.7 Action counter
| Rule | Code |
|---|---|
initial value: nextAction of the control marker, or 1 |
serve.ts:100 |
a new action takes next, then next grows by 1 |
actions.ts:156, :178 |
the new next is written before the card is posted |
actions.ts:254-255 |
| one write at a time, also after a failed write | control.ts:135-139 |
--reset-owners copies the value into the new control chat |
serve.ts:221-222, control.ts:522-527 |
| a refused call leaves the counter unchanged | actions.ts:177 |
6. Security boundaries
6.1 HTTP
Checks in order of the request listener (http.ts:19-28):
| Check | Rule | Failure | Code |
|---|---|---|---|
| bind address | 127.0.0.1 only |
http.ts:41 | |
| Host | hostname localhost, 127.0.0.1 or [::1], any port |
403, JSON-RPC error | node index.cjs:26-48, server index.cjs:1520-1558 |
| Origin | absent or empty passes; otherwise the same hostnames, any port | 403, JSON-RPC error | node index.cjs:74-96, server index.cjs:1599-1644 |
| token | Authorization equal to Bearer <token>, same length, timingSafeEqual |
401, empty body | http.ts:34-37 |
| path | pathname /mcp |
404, empty body | http.ts:22 |
| start | the agent control chat is started | 503, Retry-After: 1, empty body |
http.ts:23 |
| body size | 4 MiB | 413 | node index.cjs:395-401 |
| content type of POST | application/json |
415 | server index.cjs:1464-1467 |
| tool arguments | input schema | isError: true result |
mcp-BumlNux0.cjs:1812-1817 |
| tool results | output schema | isError: true result |
mcp-BumlNux0.cjs:1822-1829 |
Node limits: requestTimeout 0, timeout 0, headersTimeout 60 s, keepAliveTimeout 5 s (http.ts:29-32). When the client closes the response stream, the call is aborted: the request signal is aborted in toNodeHandler on close, the per-request transport is closed, and ctx.mcpReq.signal is aborted by the protocol layer (node index.cjs:299-303, server index.cjs:86-90, src-BhnGYybN.cjs:6410-6427). A result produced after the abort is discarded (src-BhnGYybN.cjs:6536).
6.2 Token and headersHelper
- The token is created once and kept in
DATA/token;--new-tokenreplaces it (files.ts:30-36). DATA/headers.jsonis rewritten at every start (files.ts:42-44).- The Claude Code entry gets the command
cat <DATA>/headers.json, ortype "<DATA>\headers.json"on Windows, with the path quoted for the shell (register.ts:50, log.ts:24-31). Only this command is written to the entry and to theclaude mcp add-jsonarguments (register.test.ts:50-61). token,headers.jsonandserver.jsonare set to mode 0600 at every write, and the data folder to 0700 at every start, except on Windows (files.ts:12-15, :55-58).
6.3 Peer-written text
| Destination | Rule | Code |
|---|---|---|
| card and reply names | folded to one line; a valid display name is plain, others a code span | safeName |
| card text lines | one code span per line; backticks become '; a leading or trailing space becomes U+00A0; blank lines empty |
codeLines, codeSpan |
| file names and paths on cards and posts | folded to one line, one code span | control.ts:585, :598, :166-168 |
| reply quote on a send card | whitespace runs to one space, first 100 characters (code points), one code span | control.ts:586 |
| link texts on a connect card | code span lines in the bytes that the largest card form leaves; the line that does not fit is cut and ends in … inside its code span; later lines left out |
control.ts:644-658 |
| mention names | valid name: @name or @'name'; others a code span |
mentionText |
| errors on cards | folded to one line, one code span | control.ts:565 |
--owners part of restart lines in the chat |
one code span | control.ts:79, :782-784 |
| terminal names and errors of actions | folded to one line | actions.ts:180-181, :333-341, control.ts:78 |
| tool results | returned as data; schema descriptions and instructions mark text, from, name, file names, downloaded files, description, welcome and commands with their labels and params as data, not instructions; the instructions take a chat's messages as prompts only when the user said so in the session, and limit what the agent shares about its user |
tools.ts:184, :188, :193, :218-219, :229-234, :397, :439 |
The server writes the tappable form /'...' only in the card's command line (control.ts:603-616), in /'revoke CHAT' of standing cards and replies (:563, :253), and in /'delete CHAT files' of the kept-files message (:301).
6.4 Chat references
| Rule | Code |
|---|---|
^[@#][1-9]\d{0,14}$: 1 to 15 digits; the core reads #(2^64 + N) as #N |
messages.ts:150-152, scenarios.test.ts:491-503 |
the control chat is refused by send, receive, download, leave and the owner commands with CHAT, and left out of list and /list |
tools.ts:1116, :1160-1177, control.ts:328-335 |
a store error of the chat read is reported as No chat CHAT. |
tools.ts:1121, :1175 |
the agent's own links are refused by connect (ownLink plans) |
tools.ts:1279, :1297, :1319 |
6.5 Owner commands
| Rule | Code |
|---|---|
source: groupRcv text items in the main scope of the control group |
serve.ts:134, control.ts:173 |
form: one line /keyword params, leading spaces allowed |
control.ts:545-550 |
| the help reply is sent to any sender; other commands are accepted from approvers only | control.ts:203-204 |
| commands run one at a time in arrival order | control.ts:177-180 |
/approve, /reject, /receive and /forward need a pending action with a card, and the action's chat when CHAT is given; /receive and /forward take downloads only, /approve the other actions only |
control.ts:227-237 |
6.6 Files
| Rule | Code |
|---|---|
send refuses files named id_*, *.pem, *.key, *.p12 or *.pfx, and files under ~/.ssh, ~/.gnupg, the data folder and ~/.simplex-mcp except the agent's own DATA/files/, each by its real path; the refusal is posted to the control chat and logged |
tools.ts:862-876, :1465-1484, control.ts:154-158 |
the file of a send is sent only when its real path, size, inode and change time at the perform are those of the request |
tools.ts:878-883 |
| a sent JPEG or PNG is a resized copy made by the core | tools.ts:885-896 |
ffmpeg and ffprobe read a video as MP4 or QuickTime, local files only, without standard input, 30 s and 64 MiB of output each |
tools.ts:1534-1552 |
a download is saved only at DATA/files/group-N/ or DATA/files/contact-N/, or for /forward at the same name under DATA/forwarded/, named by its file ID and an extension of 1 to 16 ASCII letters and digits |
messages.ts:215-223 |
a forwarded file is outside DATA/files/, so send refuses it; a download moves it into DATA/files/ after the copy for the post is made |
tools.ts:522-539, :871-876 |
a file on unknown servers gets a card that names them, also under a files approval; relays are approved only for such a card, and for every other download, and in mode host, the core checks the complete description and downloads a file on unknown servers only when the IP address is protected |
tools.ts:975, :545, :1030, control.ts:611-614 |
an accepted file that the core returns to rcvInvitation fails its download at the next check |
tools.ts:1043 |
| a received file is moved into the chat's folder at its completion, unless it is at the forwarded path, also from a member chat of a group, which has a download in memory as the main conversation does; a file without a download in memory, such as one whose transfer started before a restart, only when it has its full size; a partial file is deleted only for a download in memory, when it is no larger than the file | tools.ts:1082-1091, :1486-1492 |
6.7 Link and network
- The join link is a group link of the control chat with role
member, orobserverwith--owners; it is printed with a QR code on stdout and deleted at the first join event (control.ts:406-416, :372, :381). - At every start the server sets private message routing:
smpProxyModealways,smpProxyFallbackallowProtected(serve.ts:91-92). - With
incognito: truethe join methods connect incognito (tools.ts:721); the core then uses a random profile for that chat (PLAN, Participants).
7. Limits
| Limit | Value | Code |
|---|---|---|
| pending actions | 20 | actions.ts:11 |
| actions being performed | 20 | actions.ts:12 |
| expiry | 1 to 23 h, default 23 | cli.ts:59, :82-84 |
| connect wait for the other side | created + 23 h 50 min |
tools.ts:143 |
| send, connect and download card | 15000 bytes JSON-escaped, in its largest form | control.ts:26, :622-630 |
| error assumed in the largest card | 600 × € |
control.ts:30 |
| message as the core sends it | 15602 bytes, counted with 600 bytes of envelope; send refuses a longer one, and the post of /forward leaves out its quote of the card |
tools.ts:153-154, :1433-1436, control.ts:265-281 |
send.text |
1 to 12000 characters | tools.ts:256 |
connect.message |
1 to 2000 characters | tools.ts:244 |
send.mention |
1 to 3 unique member IDs | tools.ts:260 |
wait |
0 to 86400 s | tools.ts:245, :261, :273, :296 |
idle |
1 to 300 s, default 30 | tools.ts:246, :262, :149 |
limit |
1 to 100, default 50 | tools.ts:263, :274, :150 |
connect snapshot |
last 50 items | tools.ts:151 |
| collected items per action or call | 100, then their IDs kept as overflow until deleted | messages.ts:10, :109-110 |
| unread page | 100 items | messages.ts:9 |
| chat list read | one read of 1000000 chats | tools.ts:142 |
download wait, default |
60 s | tools.ts:146 |
| download check | every 60 s | tools.ts:147 |
| download without progress | 30 min, then cancelled | tools.ts:148 |
ffmpeg, ffprobe |
30 s, 64 MiB of output | tools.ts:152, :156 |
| stop wait | 0 to 60 s, default 10 | cli.ts:64, :85-87 |
| error details | 500 characters | log.ts:4 |
| progress interval | 60 s | messages.ts:7 |
| expiry tick | 60 s | actions.ts:10 |
| HTTP body | 4 MiB | server index.cjs:254 |
| SSE keep-alive | 15 s | mcp-BumlNux0.cjs:64 |
| agent NAME | 1 to 32 characters | cli.ts:150 |
| display name | 50 characters, mkValidName rules |
messages.ts:11-12, :337-359 |
| port | 1 to 65535, default 5226 | cli.ts:62, :94-96 |