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.
@@ -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