meshingly-mcp 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +56 -2
- package/dist/background.js +183 -0
- package/dist/connection.js +415 -0
- package/dist/lib/client.js +80 -0
- package/dist/lib/collab.js +155 -0
- package/dist/lib/config.js +98 -0
- package/dist/lib/crypto.js +143 -0
- package/dist/lib/listener.js +96 -0
- package/dist/lib/messenger.js +129 -0
- package/dist/lib/protocol.js +49 -0
- package/dist/queue.js +89 -0
- package/dist/server.js +294 -0
- package/dist/settings-cli.js +53 -0
- package/dist/settings.js +200 -0
- package/dist/watch.js +328 -0
- package/package.json +34 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Burak Uysal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,57 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Meshingly MCP
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Meshingly connects AI coding sessions through an end-to-end encrypted collaboration. You on Claude Code and a teammate on Codex, or any other tool that speaks MCP, can message each other, hand over work and review each other's changes. The relay in the middle only ever stores ciphertext. See [meshingly.com](https://meshingly.com).
|
|
4
|
+
|
|
5
|
+
This repository holds the local MCP server. It is an npm package, a Claude Code plugin and a Claude Code marketplace at once.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Claude Code, as a plugin (MCP server and skill together):
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
/plugin marketplace add DrBurakUysal/meshingly-mcp
|
|
13
|
+
/plugin install meshingly@meshingly
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Any MCP tool, through npm (needs Node.js 22.18 or newer):
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
claude mcp add meshingly -- npx -y meshingly-mcp
|
|
20
|
+
codex mcp add meshingly -- npx -y meshingly-mcp
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Other tools take the same command as a local stdio server: `npx -y meshingly-mcp`. Restart the session so the tools load, then ask your agent to set up Meshingly. It asks whether you want to start a new collaboration from scratch or join one with a code and a password.
|
|
24
|
+
|
|
25
|
+
## Tools
|
|
26
|
+
|
|
27
|
+
| Tool | What it does |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `meshingly_status` | Shows whether this project is connected |
|
|
30
|
+
| `meshingly_create` | Starts a new collaboration and returns a code and a password |
|
|
31
|
+
| `meshingly_join` | Joins with a code and a password |
|
|
32
|
+
| `meshingly_peers` | Lists the other sessions in the collaboration |
|
|
33
|
+
| `meshingly_send` | Sends an encrypted message to the others |
|
|
34
|
+
| `meshingly_receive` | Reads new messages, optionally waiting for one |
|
|
35
|
+
| `meshingly_handoff`, `meshingly_queue`, `meshingly_claim`, `meshingly_release`, `meshingly_done` | A work queue: park unfinished work, list it, take it over, give it back, finish it |
|
|
36
|
+
| `meshingly_watch_start`, `meshingly_watch_stop`, `meshingly_watch_status` | Optional background watcher that restarts queued work (see below) |
|
|
37
|
+
| `meshingly_settings` | Shows your preferences. Read-only |
|
|
38
|
+
|
|
39
|
+
## Several computers and handing work over
|
|
40
|
+
|
|
41
|
+
The same person can join one collaboration from several computers; each session is labelled with a computer name you choose (not the host name). When a session is about to run out of usage it can park its work in the queue, and any session, a teammate's or your own on another computer, can take it over.
|
|
42
|
+
|
|
43
|
+
## The optional watcher
|
|
44
|
+
|
|
45
|
+
A session that has ended cannot wake itself. If you want queued work to continue once a usage limit is back, you can run `npx -y meshingly-mcp watch` in the project folder, or turn on the `autoResume` setting. The watcher is a background process on your computer that checks the queue and starts a command you configure (default `claude -p`) with the handoff as its prompt. **It is off by default.** Nothing starts it unless you run it, enable `autoResume`, or enable `modelMayStartWatcher` to let your agent start it. By default it only runs handoffs written by your own session; `trustPeers` extends that to teammates, which means their text starts an agent on your computer, so enable it only for people you trust. Handoffs marked as a question for a person are never run.
|
|
46
|
+
|
|
47
|
+
Settings live in your home folder, not in the project (so a repository cannot ship them), and are changed in a terminal: `npx -y meshingly-mcp settings` lists them and `settings set <key> <value>` changes one. The agent can read them with `meshingly_settings` but has no tool to change them.
|
|
48
|
+
|
|
49
|
+
## What it runs, stores and sends
|
|
50
|
+
|
|
51
|
+
Unless you start the watcher described above, the server runs locally over stdio and starts no other program. Creating a collaboration generates a random key on your machine. It is stored in `.meshingly/connection.json` in your project directory (the directory ignores itself in git) and never leaves it. The only network traffic goes to `https://api.meshingly.com`: a sealed copy of the key that only the password can open, and messages encrypted with the key. Message text, session names and tool names are encrypted before they leave the machine. The relay keeps messages for seven days. Set `MESHINGLY_URL` to use another relay and `MESHINGLY_DIR` to choose where the key is stored.
|
|
52
|
+
|
|
53
|
+
Messages from other sessions are handed to your agent as untrusted input, never as instructions from you.
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { resolveSettings } from "./settings.js";
|
|
5
|
+
|
|
6
|
+
const MAX_LOG_BYTES = 1_000_000;
|
|
7
|
+
/** The watcher process writes a heartbeat this often; a pid alone is not proof that the process is still our watcher. */
|
|
8
|
+
export const BEAT_MS = 30_000;
|
|
9
|
+
const BEAT_STALE_MS = 5 * 60_000;
|
|
10
|
+
const START_GRACE_MS = 30_000;
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
const dir = (root ) => join(root, ".meshingly");
|
|
26
|
+
const recordFile = (root ) => join(dir(root), "watch.json");
|
|
27
|
+
const beatFile = (root ) => join(dir(root), "watch.beat");
|
|
28
|
+
const logFile = (root ) => join(dir(root), "watch.log");
|
|
29
|
+
const lockFile = (root ) => join(dir(root), "watch.lock");
|
|
30
|
+
export const stateFile = (root ) => join(dir(root), "watch-state.json");
|
|
31
|
+
|
|
32
|
+
/** The handoff a watcher was running when it stopped (killed, crashed): the caller gives it back. */
|
|
33
|
+
export function takeCurrentRun(root ) {
|
|
34
|
+
try {
|
|
35
|
+
if (!existsSync(stateFile(root))) return undefined;
|
|
36
|
+
const raw = JSON.parse(readFileSync(stateFile(root), "utf8")) ;
|
|
37
|
+
if (typeof raw.current !== "string") return undefined;
|
|
38
|
+
const id = raw.current;
|
|
39
|
+
delete raw.current;
|
|
40
|
+
writeFileSync(stateFile(root), JSON.stringify(raw));
|
|
41
|
+
return id;
|
|
42
|
+
} catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function writeBeat(root ) {
|
|
48
|
+
try {
|
|
49
|
+
mkdirSync(dir(root), { recursive: true });
|
|
50
|
+
writeFileSync(beatFile(root), String(Date.now()));
|
|
51
|
+
} catch {
|
|
52
|
+
/* a missed beat only makes the watcher look stale for a moment */
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function clearBeat(root ) {
|
|
57
|
+
rmSync(beatFile(root), { force: true });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function alive(pid ) {
|
|
61
|
+
try {
|
|
62
|
+
process.kill(pid, 0);
|
|
63
|
+
return true;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
// EPERM means the process exists but is not ours to signal.
|
|
66
|
+
return (err ).code === "EPERM";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Ends a process and everything it started: with `shell: true` the pid is the shell's, and a plain kill would leave the agent running. */
|
|
71
|
+
export function killTree(pid ) {
|
|
72
|
+
if (process.platform === "win32") {
|
|
73
|
+
spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Children are started detached (their own process group), so the negative pid reaches the whole group.
|
|
77
|
+
for (const target of [-pid, pid]) {
|
|
78
|
+
try {
|
|
79
|
+
process.kill(target, "SIGTERM");
|
|
80
|
+
return;
|
|
81
|
+
} catch {
|
|
82
|
+
/* try the single process, then give up: it is already gone */
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A watcher counts as running only when its pid is alive AND it has a fresh heartbeat (or has only just been started).
|
|
89
|
+
* After a crash or a reboot the pid may belong to an unrelated process; without the heartbeat check Meshingly would
|
|
90
|
+
* refuse to start a new watcher and, worse, stop would kill a stranger.
|
|
91
|
+
*/
|
|
92
|
+
export function watcherState(root ) {
|
|
93
|
+
const file = recordFile(root);
|
|
94
|
+
if (!existsSync(file)) return { running: false, logFile: logFile(root) };
|
|
95
|
+
try {
|
|
96
|
+
const record = JSON.parse(readFileSync(file, "utf8")) ;
|
|
97
|
+
if (!alive(record.pid)) return { running: false, record, logFile: logFile(root) };
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
const beat = existsSync(beatFile(root)) ? now - statSync(beatFile(root)).mtimeMs : Infinity;
|
|
100
|
+
const young = now - Date.parse(record.startedAt) < START_GRACE_MS;
|
|
101
|
+
return { running: beat < BEAT_STALE_MS || young, record, logFile: logFile(root) };
|
|
102
|
+
} catch {
|
|
103
|
+
return { running: false, logFile: logFile(root) };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Starts `meshingly-mcp watch` detached, so it keeps running after this session (and its MCP server) ended. */
|
|
108
|
+
export function startWatcher(root , entry ) {
|
|
109
|
+
const state = watcherState(root);
|
|
110
|
+
if (state.running) return `The watcher is already running (pid ${state.record .pid}, since ${state.record .startedAt}). Log: ${state.logFile}`;
|
|
111
|
+
if (!entry || !existsSync(entry)) throw new Error("cannot find the script that starts the watcher");
|
|
112
|
+
|
|
113
|
+
// Two servers starting together (two sessions in one project) must not both start a watcher: one would be orphaned.
|
|
114
|
+
mkdirSync(dir(root), { recursive: true });
|
|
115
|
+
let lock ;
|
|
116
|
+
try {
|
|
117
|
+
lock = openSync(lockFile(root), "wx");
|
|
118
|
+
} catch {
|
|
119
|
+
if (existsSync(lockFile(root)) && Date.now() - statSync(lockFile(root)).mtimeMs < 30_000) return "A watcher is being started right now.";
|
|
120
|
+
rmSync(lockFile(root), { force: true }); // left behind by a start that died
|
|
121
|
+
lock = openSync(lockFile(root), "wx");
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const again = watcherState(root);
|
|
125
|
+
if (again.running) return `The watcher is already running (pid ${again.record .pid}, since ${again.record .startedAt}). Log: ${again.logFile}`;
|
|
126
|
+
return launch(root, entry);
|
|
127
|
+
} finally {
|
|
128
|
+
closeSync(lock);
|
|
129
|
+
rmSync(lockFile(root), { force: true });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function launch(root , entry ) {
|
|
134
|
+
|
|
135
|
+
// The watcher reads the same settings itself; the flags pin what this start saw, so a later edit needs a restart.
|
|
136
|
+
const { resumeCommand: command, trustPeers: fromPeers, onQuestion, maxAttempts, checkIntervalSeconds, settleMinutes } = resolveSettings(root).values;
|
|
137
|
+
mkdirSync(dir(root), { recursive: true });
|
|
138
|
+
writeFileSync(join(dir(root), ".gitignore"), "*\n");
|
|
139
|
+
const log = logFile(root);
|
|
140
|
+
if (existsSync(log) && statSync(log).size > MAX_LOG_BYTES) rmSync(log);
|
|
141
|
+
const fd = openSync(log, "a", 0o600);
|
|
142
|
+
try {
|
|
143
|
+
const args = [
|
|
144
|
+
entry, "watch", "--dir", root, "--cmd", command, "--on-question", onQuestion,
|
|
145
|
+
"--max-attempts", String(maxAttempts), "--interval", String(checkIntervalSeconds), "--settle-minutes", String(settleMinutes),
|
|
146
|
+
...(fromPeers ? ["--from-peers"] : []),
|
|
147
|
+
];
|
|
148
|
+
const child = spawn(process.execPath, args, { detached: true, stdio: ["ignore", fd, fd], windowsHide: true, env: process.env });
|
|
149
|
+
// Without a listener a failed spawn would be an uncaught exception that takes the MCP server down with it.
|
|
150
|
+
child.on("error", (err) => console.error(`meshingly: the watcher process failed: ${err.message}`));
|
|
151
|
+
child.unref();
|
|
152
|
+
if (!child.pid) throw new Error("the watcher process did not start");
|
|
153
|
+
const record = { pid: child.pid, startedAt: new Date().toISOString(), command, fromPeers };
|
|
154
|
+
writeFileSync(recordFile(root), JSON.stringify(record, null, 2), { mode: 0o600 });
|
|
155
|
+
} finally {
|
|
156
|
+
closeSync(fd);
|
|
157
|
+
}
|
|
158
|
+
return [
|
|
159
|
+
`Watcher started. It checks the queue every ${checkIntervalSeconds} s and, once a handoff is ready, runs: ${command}`,
|
|
160
|
+
onQuestion === "recommended" ? "If a run would ask a question it takes the recommended option and notes the assumption." : "If a run would need an answer it parks the question for a person.",
|
|
161
|
+
fromPeers ? "It also runs handoffs written by other sessions (trustPeers)." : "It runs only handoffs written by this project's own session.",
|
|
162
|
+
`It keeps running after this session ends, for as long as this computer is on. Log: ${log}. Stop it with meshingly_watch_stop. Settings: meshingly_settings.`,
|
|
163
|
+
].join("\n");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function stopWatcher(root ) {
|
|
167
|
+
const state = watcherState(root);
|
|
168
|
+
rmSync(recordFile(root), { force: true });
|
|
169
|
+
if (!state.running || !state.record) {
|
|
170
|
+
clearBeat(root);
|
|
171
|
+
return "No watcher is running.";
|
|
172
|
+
}
|
|
173
|
+
const { pid } = state.record;
|
|
174
|
+
killTree(pid);
|
|
175
|
+
clearBeat(root);
|
|
176
|
+
return `Watcher stopped (pid ${pid}).`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function describeWatcher(root ) {
|
|
180
|
+
const s = watcherState(root);
|
|
181
|
+
if (!s.running || !s.record) return "No watcher is running.";
|
|
182
|
+
return `Watcher running (pid ${s.record.pid}, since ${s.record.startedAt}), command: ${s.record.command}${s.record.fromPeers ? ", also for peers' handoffs" : ", own handoffs only"}. Log: ${s.logFile}`;
|
|
183
|
+
}
|