multi-agent-collaboration-mcp 0.12.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 +202 -0
- package/README.md +217 -0
- package/dist/bounded-lines.js +78 -0
- package/dist/build-info.json +1 -0
- package/dist/check.js +428 -0
- package/dist/db.js +3102 -0
- package/dist/index.js +2241 -0
- package/dist/poller.js +419 -0
- package/dist/unicode.js +78 -0
- package/package.json +51 -0
- package/scripts/prepare.mjs +82 -0
- package/scripts/refresh-mcp.sh +228 -0
- package/scripts/stamp-build.mjs +93 -0
- package/scripts/wait-for-updates.sh +39 -0
- package/web/index.html +3117 -0
- package/web/server.mjs +1016 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Re-register the freshly built agent-chat MCP server with supported AI CLIs.
|
|
3
|
+
# The npm script runs the build first; this file also validates the artifact so
|
|
4
|
+
# invoking it directly fails clearly instead of installing a broken command.
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
|
|
7
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
8
|
+
SERVER_PATH="$ROOT_DIR/dist/index.js"
|
|
9
|
+
MCP_NAME="${AGENT_CHAT_MCP_NAME:-agent-chat}"
|
|
10
|
+
NODE_BIN="${AGENT_CHAT_NODE_BIN:-${npm_node_execpath:-}}"
|
|
11
|
+
FORCE_REREGISTER="${AGENT_CHAT_FORCE_REREGISTER:-0}"
|
|
12
|
+
|
|
13
|
+
if [[ -z "$NODE_BIN" ]]; then
|
|
14
|
+
NODE_BIN="$(command -v node || true)"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
if [[ -z "$MCP_NAME" ]]; then
|
|
18
|
+
echo "refresh-mcp: AGENT_CHAT_MCP_NAME cannot be empty" >&2
|
|
19
|
+
exit 2
|
|
20
|
+
fi
|
|
21
|
+
if [[ -z "$NODE_BIN" || ! -x "$NODE_BIN" ]]; then
|
|
22
|
+
echo "refresh-mcp: Node.js was not found; install Node 22+ or set AGENT_CHAT_NODE_BIN" >&2
|
|
23
|
+
exit 2
|
|
24
|
+
fi
|
|
25
|
+
if [[ "$FORCE_REREGISTER" != "0" && "$FORCE_REREGISTER" != "1" ]]; then
|
|
26
|
+
echo "refresh-mcp: AGENT_CHAT_FORCE_REREGISTER must be 0 or 1" >&2
|
|
27
|
+
exit 2
|
|
28
|
+
fi
|
|
29
|
+
if [[ ! -f "$SERVER_PATH" ]]; then
|
|
30
|
+
echo "refresh-mcp: $SERVER_PATH not found; run 'npm run build' first" >&2
|
|
31
|
+
exit 2
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
node_major="$("$NODE_BIN" -p 'Number(process.versions.node.split(".")[0])' 2>/dev/null || true)"
|
|
35
|
+
if [[ ! "$node_major" =~ ^[0-9]+$ || "$node_major" -lt 22 ]]; then
|
|
36
|
+
echo "refresh-mcp: Node 22+ is required (found: $("$NODE_BIN" --version 2>/dev/null || echo unknown))" >&2
|
|
37
|
+
exit 2
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
refreshed=0
|
|
41
|
+
failed=0
|
|
42
|
+
skipped=0
|
|
43
|
+
|
|
44
|
+
remove_registration() {
|
|
45
|
+
local label=$1
|
|
46
|
+
shift
|
|
47
|
+
echo "[$label] removing existing '$MCP_NAME' registration..."
|
|
48
|
+
if ! "$@"; then
|
|
49
|
+
# Claude exits nonzero when the name is absent. Continue because add is the
|
|
50
|
+
# authoritative step and will still expose permission/configuration errors.
|
|
51
|
+
echo "[$label] no removable user registration found; continuing" >&2
|
|
52
|
+
fi
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
add_registration() {
|
|
56
|
+
local label=$1
|
|
57
|
+
shift
|
|
58
|
+
echo "[$label] adding '$MCP_NAME' -> $NODE_BIN $SERVER_PATH"
|
|
59
|
+
if "$@"; then
|
|
60
|
+
refreshed=$((refreshed + 1))
|
|
61
|
+
else
|
|
62
|
+
echo "[$label] failed to add '$MCP_NAME'" >&2
|
|
63
|
+
failed=$((failed + 1))
|
|
64
|
+
fi
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Both supported CLIs print one command and one args line for stdio servers.
|
|
68
|
+
# Parse those fields and require exact values: a substring check accepts stale
|
|
69
|
+
# targets such as dist/index.js.old or an unrelated extra argument that merely
|
|
70
|
+
# mentions this checkout. Unknown future output formats fail closed and can be
|
|
71
|
+
# replaced explicitly with AGENT_CHAT_FORCE_REREGISTER=1.
|
|
72
|
+
registration_targets_checkout() {
|
|
73
|
+
local registration=$1
|
|
74
|
+
local line
|
|
75
|
+
local registered_command=""
|
|
76
|
+
local registered_args=""
|
|
77
|
+
while IFS= read -r line; do
|
|
78
|
+
line="${line#"${line%%[![:space:]]*}"}"
|
|
79
|
+
case "$line" in
|
|
80
|
+
command:\ *) registered_command="${line#command: }" ;;
|
|
81
|
+
Command:\ *) registered_command="${line#Command: }" ;;
|
|
82
|
+
args:\ *) registered_args="${line#args: }" ;;
|
|
83
|
+
Args:\ *) registered_args="${line#Args: }" ;;
|
|
84
|
+
esac
|
|
85
|
+
done <<< "$registration"
|
|
86
|
+
[[ "$registered_command" == "$NODE_BIN" && "$registered_args" == "$SERVER_PATH" ]]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if command -v codex >/dev/null 2>&1; then
|
|
90
|
+
if [[ "$FORCE_REREGISTER" == "0" ]] &&
|
|
91
|
+
codex_registration="$(codex mcp get "$MCP_NAME" 2>/dev/null)"; then
|
|
92
|
+
if registration_targets_checkout "$codex_registration"; then
|
|
93
|
+
echo "[Codex] existing '$MCP_NAME' registration already targets this checkout; build refreshed in place"
|
|
94
|
+
refreshed=$((refreshed + 1))
|
|
95
|
+
else
|
|
96
|
+
echo "[Codex] existing '$MCP_NAME' registration does not point at $SERVER_PATH; left unchanged. Set AGENT_CHAT_FORCE_REREGISTER=1 to replace it." >&2
|
|
97
|
+
failed=$((failed + 1))
|
|
98
|
+
fi
|
|
99
|
+
else
|
|
100
|
+
remove_registration "Codex" codex mcp remove "$MCP_NAME"
|
|
101
|
+
add_registration "Codex" \
|
|
102
|
+
codex mcp add "$MCP_NAME" -- "$NODE_BIN" "$SERVER_PATH"
|
|
103
|
+
fi
|
|
104
|
+
else
|
|
105
|
+
echo "[Codex] skipped: 'codex' is not installed" >&2
|
|
106
|
+
skipped=$((skipped + 1))
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# Antigravity (agy) has no MCP-management CLI. It discovers global servers in
|
|
110
|
+
# ~/.gemini/config/mcp_config.json, which is separate from Gemini CLI's user
|
|
111
|
+
# settings. Update that JSON atomically and preserve every unrelated field and
|
|
112
|
+
# server. AGENT_CHAT_AGY_CONFIG provides an isolated path for tests.
|
|
113
|
+
if command -v agy >/dev/null 2>&1; then
|
|
114
|
+
if [[ -n "${AGENT_CHAT_AGY_CONFIG:-}" ]]; then
|
|
115
|
+
AGY_CONFIG="$AGENT_CHAT_AGY_CONFIG"
|
|
116
|
+
elif [[ -n "${HOME:-}" ]]; then
|
|
117
|
+
AGY_CONFIG="$HOME/.gemini/config/mcp_config.json"
|
|
118
|
+
else
|
|
119
|
+
AGY_CONFIG=""
|
|
120
|
+
fi
|
|
121
|
+
if [[ -z "$AGY_CONFIG" ]]; then
|
|
122
|
+
echo "[Antigravity/agy] failed: HOME is unset; set AGENT_CHAT_AGY_CONFIG" >&2
|
|
123
|
+
failed=$((failed + 1))
|
|
124
|
+
elif [[ "$AGY_CONFIG" != /* ]]; then
|
|
125
|
+
echo "[Antigravity/agy] failed: config path must be absolute: $AGY_CONFIG" >&2
|
|
126
|
+
failed=$((failed + 1))
|
|
127
|
+
else
|
|
128
|
+
echo "[Antigravity/agy] refreshing '$MCP_NAME' in $AGY_CONFIG"
|
|
129
|
+
if "$NODE_BIN" - "$AGY_CONFIG" "$MCP_NAME" "$NODE_BIN" "$SERVER_PATH" <<'NODE'
|
|
130
|
+
const fs = require("node:fs");
|
|
131
|
+
const pathModule = require("node:path");
|
|
132
|
+
|
|
133
|
+
const [requestedPath, name, command, serverPath] = process.argv.slice(2);
|
|
134
|
+
const exists = fs.existsSync(requestedPath);
|
|
135
|
+
const configPath =
|
|
136
|
+
exists && fs.lstatSync(requestedPath).isSymbolicLink()
|
|
137
|
+
? fs.realpathSync(requestedPath)
|
|
138
|
+
: requestedPath;
|
|
139
|
+
|
|
140
|
+
let config = {};
|
|
141
|
+
if (exists) {
|
|
142
|
+
const source = fs.readFileSync(configPath, "utf8");
|
|
143
|
+
config = source.trim().length === 0 ? {} : JSON.parse(source);
|
|
144
|
+
}
|
|
145
|
+
if (config === null || typeof config !== "object" || Array.isArray(config)) {
|
|
146
|
+
throw new Error(`${configPath} must contain a JSON object`);
|
|
147
|
+
}
|
|
148
|
+
if (config.mcpServers === undefined) config.mcpServers = {};
|
|
149
|
+
if (
|
|
150
|
+
config.mcpServers === null ||
|
|
151
|
+
typeof config.mcpServers !== "object" ||
|
|
152
|
+
Array.isArray(config.mcpServers)
|
|
153
|
+
) {
|
|
154
|
+
throw new Error(`${configPath}: mcpServers must be a JSON object`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Preserve target-specific environment, timeout, and tool-filter fields. A
|
|
158
|
+
// refresh changes only the executable pair; deleting/recreating this object
|
|
159
|
+
// silently discarded user configuration.
|
|
160
|
+
const prior = config.mcpServers[name];
|
|
161
|
+
const retained =
|
|
162
|
+
prior !== null && typeof prior === "object" && !Array.isArray(prior)
|
|
163
|
+
? prior
|
|
164
|
+
: {};
|
|
165
|
+
config.mcpServers[name] = { ...retained, command, args: [serverPath] };
|
|
166
|
+
|
|
167
|
+
const dir = pathModule.dirname(configPath);
|
|
168
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
169
|
+
const mode = exists ? fs.statSync(configPath).mode & 0o777 : 0o600;
|
|
170
|
+
const tempPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
|
|
171
|
+
try {
|
|
172
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(config, null, 2)}\n`, {
|
|
173
|
+
encoding: "utf8",
|
|
174
|
+
mode,
|
|
175
|
+
});
|
|
176
|
+
fs.chmodSync(tempPath, mode);
|
|
177
|
+
fs.renameSync(tempPath, configPath);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
try {
|
|
180
|
+
fs.unlinkSync(tempPath);
|
|
181
|
+
} catch {}
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
NODE
|
|
185
|
+
then
|
|
186
|
+
refreshed=$((refreshed + 1))
|
|
187
|
+
else
|
|
188
|
+
echo "[Antigravity/agy] failed to refresh '$MCP_NAME'" >&2
|
|
189
|
+
failed=$((failed + 1))
|
|
190
|
+
fi
|
|
191
|
+
fi
|
|
192
|
+
else
|
|
193
|
+
echo "[Antigravity/agy] skipped: 'agy' is not installed" >&2
|
|
194
|
+
skipped=$((skipped + 1))
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
if command -v claude >/dev/null 2>&1; then
|
|
198
|
+
if [[ "$FORCE_REREGISTER" == "0" ]] &&
|
|
199
|
+
claude_registration="$(claude mcp get "$MCP_NAME" 2>/dev/null)"; then
|
|
200
|
+
if registration_targets_checkout "$claude_registration"; then
|
|
201
|
+
echo "[Claude] existing '$MCP_NAME' registration already targets this checkout; build refreshed in place"
|
|
202
|
+
refreshed=$((refreshed + 1))
|
|
203
|
+
else
|
|
204
|
+
echo "[Claude] existing '$MCP_NAME' registration does not point at $SERVER_PATH; left unchanged. Set AGENT_CHAT_FORCE_REREGISTER=1 to replace it." >&2
|
|
205
|
+
failed=$((failed + 1))
|
|
206
|
+
fi
|
|
207
|
+
else
|
|
208
|
+
remove_registration "Claude" \
|
|
209
|
+
claude mcp remove --scope user "$MCP_NAME"
|
|
210
|
+
add_registration "Claude" \
|
|
211
|
+
claude mcp add --scope user --transport stdio \
|
|
212
|
+
"$MCP_NAME" -- "$NODE_BIN" "$SERVER_PATH"
|
|
213
|
+
fi
|
|
214
|
+
else
|
|
215
|
+
echo "[Claude] skipped: 'claude' is not installed" >&2
|
|
216
|
+
skipped=$((skipped + 1))
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
echo
|
|
220
|
+
echo "MCP refresh complete: $refreshed refreshed, $skipped skipped, $failed failed."
|
|
221
|
+
echo "Restart or reconnect already-running CLI sessions so they spawn the new server build."
|
|
222
|
+
|
|
223
|
+
if [[ "$failed" -gt 0 ]]; then
|
|
224
|
+
exit 1
|
|
225
|
+
fi
|
|
226
|
+
if [[ "$refreshed" -eq 0 ]]; then
|
|
227
|
+
exit 2
|
|
228
|
+
fi
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Stamp the deployed build's identity into dist/build-info.json after tsc, so the
|
|
3
|
+
// server_info tool can report the exact commit that produced the running binary.
|
|
4
|
+
// Done at build time, not runtime: a runtime `git rev-parse` would report the
|
|
5
|
+
// source HEAD, which masks the "edited source, forgot to rebuild" skew this exists
|
|
6
|
+
// to surface. dist/build-info.json is gitignored (dist/ is not tracked).
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { lstatSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { dirname, join, relative } from "node:path";
|
|
12
|
+
|
|
13
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
14
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
15
|
+
|
|
16
|
+
let commit = "unknown";
|
|
17
|
+
try {
|
|
18
|
+
commit = execFileSync("git", ["rev-parse", "--short", "HEAD"], {
|
|
19
|
+
cwd: root,
|
|
20
|
+
encoding: "utf8",
|
|
21
|
+
timeout: 10_000,
|
|
22
|
+
}).trim();
|
|
23
|
+
const dirty =
|
|
24
|
+
execFileSync("git", ["status", "--porcelain"], {
|
|
25
|
+
cwd: root,
|
|
26
|
+
encoding: "utf8",
|
|
27
|
+
timeout: 10_000,
|
|
28
|
+
}).trim().length > 0;
|
|
29
|
+
if (dirty) commit += "-dirty"; // built from an uncommitted tree
|
|
30
|
+
} catch {
|
|
31
|
+
// Not a git checkout (e.g. dist copied without .git); leave "unknown".
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// A timestamp alone makes every no-change rebuild tell already-running MCP
|
|
35
|
+
// processes they are stale. Hash the executable JS instead: reconnect only
|
|
36
|
+
// when the deployed behavior actually differs. Keep the timestamp for human
|
|
37
|
+
// diagnostics and as a compatibility fallback for stamps made by old builds.
|
|
38
|
+
const dist = join(root, "dist");
|
|
39
|
+
const files = [];
|
|
40
|
+
const walk = (dir) => {
|
|
41
|
+
for (const name of readdirSync(dir)) {
|
|
42
|
+
const path = join(dir, name);
|
|
43
|
+
const stat = lstatSync(path);
|
|
44
|
+
// dist is generated output. Never follow a stray symlink during a build:
|
|
45
|
+
// a loop or a link to a large tree could turn stamping into an unbounded
|
|
46
|
+
// traversal, and the linked bytes are not part of the packaged artifact.
|
|
47
|
+
if (stat.isSymbolicLink()) continue;
|
|
48
|
+
if (stat.isDirectory()) walk(path);
|
|
49
|
+
else if (name.endsWith(".js")) files.push(path);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
walk(dist);
|
|
53
|
+
// Code-unit comparison is deterministic across LANG/LC_ALL. localeCompare
|
|
54
|
+
// made identical artifacts hash differently under locales such as cs_CZ.
|
|
55
|
+
files.sort((a, b) => {
|
|
56
|
+
const ar = relative(root, a);
|
|
57
|
+
const br = relative(root, b);
|
|
58
|
+
return ar < br ? -1 : ar > br ? 1 : 0;
|
|
59
|
+
});
|
|
60
|
+
const hash = createHash("sha256");
|
|
61
|
+
const runtimeDependencies = Object.keys(pkg.dependencies ?? {})
|
|
62
|
+
.sort()
|
|
63
|
+
.map((name) => {
|
|
64
|
+
try {
|
|
65
|
+
const installed = JSON.parse(
|
|
66
|
+
readFileSync(join(root, "node_modules", name, "package.json"), "utf8"),
|
|
67
|
+
);
|
|
68
|
+
return [name, installed.version];
|
|
69
|
+
} catch {
|
|
70
|
+
// A normal build has dependencies installed. Keep the declared range in
|
|
71
|
+
// the identity if a nonstandard packager compiles without node_modules.
|
|
72
|
+
return [name, pkg.dependencies[name]];
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
hash.update(
|
|
76
|
+
JSON.stringify({ version: pkg.version, dependencies: runtimeDependencies }),
|
|
77
|
+
);
|
|
78
|
+
hash.update("\0");
|
|
79
|
+
for (const path of files) {
|
|
80
|
+
hash.update(relative(root, path));
|
|
81
|
+
hash.update("\0");
|
|
82
|
+
hash.update(readFileSync(path));
|
|
83
|
+
hash.update("\0");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const info = {
|
|
87
|
+
version: pkg.version,
|
|
88
|
+
commit,
|
|
89
|
+
built_at: new Date().toISOString(),
|
|
90
|
+
artifact_hash: hash.digest("hex"),
|
|
91
|
+
};
|
|
92
|
+
writeFileSync(join(root, "dist", "build-info.json"), JSON.stringify(info) + "\n");
|
|
93
|
+
console.log("stamped dist/build-info.json:", JSON.stringify(info));
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Compatibility entry point for old poller commands. It replaces itself with
|
|
3
|
+
# the single-process Node watcher; there is no shell polling loop or child.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
POLLER="$SCRIPT_DIR/../dist/poller.js"
|
|
8
|
+
node_bin=node
|
|
9
|
+
args=()
|
|
10
|
+
|
|
11
|
+
while [[ $# -gt 0 ]]; do
|
|
12
|
+
case "$1" in
|
|
13
|
+
--node)
|
|
14
|
+
[[ $# -ge 2 ]] || { echo "wait-for-updates: --node needs a value" >&2; exit 2; }
|
|
15
|
+
node_bin="$2"
|
|
16
|
+
shift 2
|
|
17
|
+
;;
|
|
18
|
+
--node=*)
|
|
19
|
+
node_bin="${1#*=}"
|
|
20
|
+
shift
|
|
21
|
+
;;
|
|
22
|
+
*)
|
|
23
|
+
args+=("$1")
|
|
24
|
+
shift
|
|
25
|
+
;;
|
|
26
|
+
esac
|
|
27
|
+
done
|
|
28
|
+
|
|
29
|
+
[[ -n "$node_bin" ]] || { echo "wait-for-updates: empty Node executable" >&2; exit 2; }
|
|
30
|
+
command -v "$node_bin" >/dev/null 2>&1 || {
|
|
31
|
+
echo "wait-for-updates: Node executable not found: $node_bin" >&2
|
|
32
|
+
exit 2
|
|
33
|
+
}
|
|
34
|
+
[[ -f "$POLLER" ]] || { echo "wait-for-updates: $POLLER not found; run 'npm run build'" >&2; exit 2; }
|
|
35
|
+
if [[ ${#args[@]} -eq 0 ]]; then
|
|
36
|
+
exec "$node_bin" "$POLLER"
|
|
37
|
+
else
|
|
38
|
+
exec "$node_bin" "$POLLER" "${args[@]}"
|
|
39
|
+
fi
|