opengrok-mcp-server 3.3.3
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/CHANGELOG.md +460 -0
- package/CONTRIBUTING.md +281 -0
- package/LICENSE +131 -0
- package/MCP_CLIENTS.md +348 -0
- package/README.md +234 -0
- package/images/icon.png +0 -0
- package/out/extension.js +1 -0
- package/out/server/main.js +255 -0
- package/out/webview/configManager.html +759 -0
- package/package.json +157 -0
- package/scripts/build-vsix.js +47 -0
- package/scripts/generate-release-notes.js +70 -0
- package/scripts/install.sh +136 -0
- package/scripts/opengrok-mcp-wrapper.cmd +5 -0
- package/scripts/opengrok-mcp-wrapper.ps1 +377 -0
- package/scripts/opengrok-mcp-wrapper.sh +379 -0
- package/scripts/package-server.js +140 -0
- package/scripts/release.ps1 +137 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# opengrok-mcp-wrapper.sh
|
|
3
|
+
#
|
|
4
|
+
# Credential wrapper for the OpenGrok MCP server.
|
|
5
|
+
# Resolves credentials from OS keychain or encrypted file, then exec's the server.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# opengrok-mcp-wrapper.sh --setup # Interactive one-time credential setup
|
|
9
|
+
# opengrok-mcp-wrapper.sh # Silent mode — spawned by MCP clients
|
|
10
|
+
#
|
|
11
|
+
# Environment overrides:
|
|
12
|
+
# OPENGROK_BIN Path to opengrok-mcp binary (default: same dir as this script)
|
|
13
|
+
# OPENGROK_CONFIG_DIR Config directory (default: ~/.config/opengrok-mcp)
|
|
14
|
+
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
18
|
+
OPENGROK_BIN="${OPENGROK_BIN:-$SCRIPT_DIR/opengrok-mcp}"
|
|
19
|
+
CONFIG_DIR="${OPENGROK_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opengrok-mcp}"
|
|
20
|
+
CONFIG_FILE="$CONFIG_DIR/config"
|
|
21
|
+
ENC_FILE="$CONFIG_DIR/credentials.enc"
|
|
22
|
+
DOTENV_FILE="$CONFIG_DIR/.env"
|
|
23
|
+
SALT_FILE="$CONFIG_DIR/.salt"
|
|
24
|
+
|
|
25
|
+
# ── Encryption helpers ────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
# Ensure a random salt file exists (created once, reused for key derivation)
|
|
28
|
+
_ensure_salt() {
|
|
29
|
+
if [[ ! -f "$SALT_FILE" ]] || [[ ! -s "$SALT_FILE" ]]; then
|
|
30
|
+
mkdir -p "$CONFIG_DIR"
|
|
31
|
+
dd if=/dev/urandom bs=16 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n' > "$SALT_FILE"
|
|
32
|
+
chmod 600 "$SALT_FILE"
|
|
33
|
+
if [[ ! -s "$SALT_FILE" ]]; then
|
|
34
|
+
echo "opengrok-mcp: ⚠ Failed to generate cryptographic salt — encryption will use weaker key material." >&2
|
|
35
|
+
rm -f "$SALT_FILE"
|
|
36
|
+
return 1
|
|
37
|
+
fi
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Derive a passphrase from machine-id + username + random salt (unique per install)
|
|
42
|
+
_key_material() {
|
|
43
|
+
local machine_id salt
|
|
44
|
+
machine_id="$(cat /etc/machine-id 2>/dev/null || hostname 2>/dev/null || echo 'default')"
|
|
45
|
+
salt="$(cat "$SALT_FILE" 2>/dev/null || echo '')"
|
|
46
|
+
printf '%s%s%s' "$machine_id" "$USER" "$salt"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
encrypt_credentials() {
|
|
50
|
+
local password="$1"
|
|
51
|
+
local out_file="$2"
|
|
52
|
+
local kmat
|
|
53
|
+
_ensure_salt
|
|
54
|
+
kmat="$(_key_material)"
|
|
55
|
+
printf '%s' "$password" \
|
|
56
|
+
| openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -pass "pass:${kmat}" -out "$out_file" 2>/dev/null \
|
|
57
|
+
|| { echo "opengrok-mcp: openssl encryption failed." >&2; return 1; }
|
|
58
|
+
chmod 600 "$out_file"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
decrypt_credentials() {
|
|
62
|
+
local in_file="$1"
|
|
63
|
+
local kmat
|
|
64
|
+
kmat="$(_key_material)"
|
|
65
|
+
openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -d \
|
|
66
|
+
-pass "pass:${kmat}" -in "$in_file" 2>/dev/null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Legacy key material (without salt) for migration from pre-salt encrypted files
|
|
70
|
+
_key_material_legacy() {
|
|
71
|
+
local machine_id
|
|
72
|
+
machine_id="$(cat /etc/machine-id 2>/dev/null || hostname 2>/dev/null || echo 'default')"
|
|
73
|
+
printf '%s%s' "$machine_id" "$USER"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# Migrate encrypted credentials from legacy (no salt) to salted key derivation
|
|
77
|
+
_migrate_credentials() {
|
|
78
|
+
if [[ -f "$ENC_FILE" ]] && [[ ! -f "$SALT_FILE" ]] && command -v openssl >/dev/null 2>&1; then
|
|
79
|
+
local legacy_kmat pw
|
|
80
|
+
legacy_kmat="$(_key_material_legacy)"
|
|
81
|
+
pw="$(openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -d \
|
|
82
|
+
-pass "pass:${legacy_kmat}" -in "$ENC_FILE" 2>/dev/null)" || return 0
|
|
83
|
+
if [[ -n "$pw" ]]; then
|
|
84
|
+
# Re-encrypt with salted key (encrypt_credentials calls _ensure_salt)
|
|
85
|
+
encrypt_credentials "$pw" "$ENC_FILE" || true
|
|
86
|
+
fi
|
|
87
|
+
fi
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# ── Keychain helpers ──────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
_macos_keychain_store() {
|
|
93
|
+
local username="$1" password="$2"
|
|
94
|
+
# Delete existing entry first (ignore failure if not found)
|
|
95
|
+
security delete-generic-password -s opengrok-mcp -a "$username" 2>/dev/null || true
|
|
96
|
+
security add-generic-password -s opengrok-mcp -a "$username" -w "$password" 2>/dev/null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
_macos_keychain_read() {
|
|
100
|
+
local username="$1"
|
|
101
|
+
security find-generic-password -s opengrok-mcp -a "$username" -w 2>/dev/null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_secrettool_available() {
|
|
105
|
+
# Return 0 only if secret-tool is installed AND D-Bus/keyring is reachable
|
|
106
|
+
command -v secret-tool >/dev/null 2>&1 || return 1
|
|
107
|
+
# Probe: a failed lookup returns exit 1; a D-Bus error returns exit >1
|
|
108
|
+
secret-tool lookup service opengrok-mcp-probe username probe 2>/dev/null
|
|
109
|
+
local rc=$?
|
|
110
|
+
[[ $rc -eq 0 || $rc -eq 1 ]] # 0=found, 1=not found — both mean D-Bus works
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
_secrettool_store() {
|
|
114
|
+
local username="$1" password="$2"
|
|
115
|
+
printf '%s' "$password" \
|
|
116
|
+
| secret-tool store --label="OpenGrok MCP credentials" \
|
|
117
|
+
service opengrok-mcp username "$username" 2>/dev/null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_secrettool_read() {
|
|
121
|
+
local username="$1"
|
|
122
|
+
secret-tool lookup service opengrok-mcp username "$username" 2>/dev/null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# ── Safe .env / config file parser ───────────────────────────────────────────
|
|
126
|
+
# Does NOT use `source` — prevents arbitrary code execution.
|
|
127
|
+
_load_file_vars() {
|
|
128
|
+
local file="$1"
|
|
129
|
+
[[ -f "$file" ]] || return 0
|
|
130
|
+
while IFS='=' read -r key value; do
|
|
131
|
+
# Trim whitespace from key
|
|
132
|
+
key="${key#"${key%%[! ]*}"}"
|
|
133
|
+
key="${key%"${key##*[! ]}"}"
|
|
134
|
+
# Skip comments and blank lines
|
|
135
|
+
[[ -z "$key" || "$key" == \#* ]] && continue
|
|
136
|
+
# Only export safe variable names (alphanumeric + underscore, starts with letter)
|
|
137
|
+
if [[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
|
|
138
|
+
export "$key"="$value"
|
|
139
|
+
fi
|
|
140
|
+
done < "$file"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
144
|
+
# --setup mode: interactive credential setup
|
|
145
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
146
|
+
|
|
147
|
+
_run_setup() {
|
|
148
|
+
echo ""
|
|
149
|
+
echo "╔════════════════════════════════════════════╗"
|
|
150
|
+
echo "║ OpenGrok MCP — Credential Setup ║"
|
|
151
|
+
echo "╚════════════════════════════════════════════╝"
|
|
152
|
+
echo ""
|
|
153
|
+
|
|
154
|
+
# Prompt for configuration
|
|
155
|
+
local default_url="https://opengrok.example.com/source/"
|
|
156
|
+
read -r -p "OpenGrok Base URL [${default_url}]: " input_url
|
|
157
|
+
local base_url="${input_url:-$default_url}"
|
|
158
|
+
|
|
159
|
+
read -r -p "Username [${USER}]: " input_user
|
|
160
|
+
local username="${input_user:-$USER}"
|
|
161
|
+
|
|
162
|
+
local password=""
|
|
163
|
+
while [[ -z "$password" ]]; do
|
|
164
|
+
read -r -s -p "Password: " password
|
|
165
|
+
echo ""
|
|
166
|
+
if [[ -z "$password" ]]; then
|
|
167
|
+
echo "Password cannot be empty. Please try again."
|
|
168
|
+
fi
|
|
169
|
+
done
|
|
170
|
+
|
|
171
|
+
read -r -p "Verify SSL certificates? [Y/n]: " input_ssl
|
|
172
|
+
local verify_ssl="true"
|
|
173
|
+
if [[ "${input_ssl,,}" == "n" || "${input_ssl,,}" == "no" ]]; then
|
|
174
|
+
verify_ssl="false"
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
echo ""
|
|
178
|
+
echo "Testing connection..."
|
|
179
|
+
|
|
180
|
+
# Quick connectivity test using curl (avoid depending on the server itself)
|
|
181
|
+
local test_url="${base_url%/}/api/v1/projects"
|
|
182
|
+
local curl_args=(-fsSL --max-time 10 -u "${username}:${password}" -o /dev/null -w "%{http_code}")
|
|
183
|
+
if [[ "$verify_ssl" == "false" ]]; then
|
|
184
|
+
curl_args+=(-k)
|
|
185
|
+
fi
|
|
186
|
+
local http_code
|
|
187
|
+
http_code="$(curl "${curl_args[@]}" "$test_url" 2>/dev/null)" || http_code="000"
|
|
188
|
+
|
|
189
|
+
case "$http_code" in
|
|
190
|
+
200|201) echo " ✓ Connection successful (HTTP $http_code)" ;;
|
|
191
|
+
401|403) echo " ✗ Authentication failed (HTTP $http_code). Check username and password." ; return 1 ;;
|
|
192
|
+
000) echo " ✗ Could not reach $test_url. Check the URL and your network." ; return 1 ;;
|
|
193
|
+
*) echo " ! Unexpected response (HTTP $http_code). Proceeding anyway." ;;
|
|
194
|
+
esac
|
|
195
|
+
|
|
196
|
+
# ── Store credentials in the most secure available store ──────────────────
|
|
197
|
+
echo ""
|
|
198
|
+
echo "Storing credentials..."
|
|
199
|
+
|
|
200
|
+
local stored=false
|
|
201
|
+
|
|
202
|
+
case "$(uname -s)" in
|
|
203
|
+
Darwin)
|
|
204
|
+
if _macos_keychain_store "$username" "$password"; then
|
|
205
|
+
echo " ✓ Stored in macOS Keychain"
|
|
206
|
+
stored=true
|
|
207
|
+
else
|
|
208
|
+
echo " ! Keychain write failed, falling back to encrypted file."
|
|
209
|
+
fi
|
|
210
|
+
;;
|
|
211
|
+
Linux)
|
|
212
|
+
if _secrettool_available; then
|
|
213
|
+
if _secrettool_store "$username" "$password"; then
|
|
214
|
+
echo " ✓ Stored in GNOME Keyring / KDE Wallet via secret-tool"
|
|
215
|
+
stored=true
|
|
216
|
+
else
|
|
217
|
+
echo " ! secret-tool write failed, falling back to encrypted file."
|
|
218
|
+
fi
|
|
219
|
+
else
|
|
220
|
+
echo " ℹ No desktop keychain available (headless/SSH session)."
|
|
221
|
+
fi
|
|
222
|
+
;;
|
|
223
|
+
esac
|
|
224
|
+
|
|
225
|
+
if [[ "$stored" == "false" ]]; then
|
|
226
|
+
if command -v openssl >/dev/null 2>&1; then
|
|
227
|
+
mkdir -p "$CONFIG_DIR"
|
|
228
|
+
if encrypt_credentials "$password" "$ENC_FILE"; then
|
|
229
|
+
echo " ✓ Stored in encrypted file: $ENC_FILE"
|
|
230
|
+
echo " (AES-256-CBC, key derived from machine-id + username)"
|
|
231
|
+
stored=true
|
|
232
|
+
else
|
|
233
|
+
echo " ! Encryption failed."
|
|
234
|
+
fi
|
|
235
|
+
fi
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
if [[ "$stored" == "false" ]]; then
|
|
239
|
+
# Last resort: plaintext .env (warn user)
|
|
240
|
+
mkdir -p "$CONFIG_DIR"
|
|
241
|
+
printf '# Created: %s\nOPENGROK_PASSWORD=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$password" > "$DOTENV_FILE"
|
|
242
|
+
chmod 600 "$DOTENV_FILE"
|
|
243
|
+
echo " ⚠ Fallback: password saved to $DOTENV_FILE (plaintext)"
|
|
244
|
+
echo " Consider installing openssl or configuring a keychain for better security."
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
# ── Store non-secret config (URL, username, SSL) ──────────────────────────
|
|
248
|
+
mkdir -p "$CONFIG_DIR"
|
|
249
|
+
cat > "$CONFIG_FILE" <<EOF
|
|
250
|
+
# OpenGrok MCP non-secret configuration
|
|
251
|
+
# Generated by: opengrok-mcp-wrapper.sh --setup
|
|
252
|
+
OPENGROK_BASE_URL=${base_url}
|
|
253
|
+
OPENGROK_USERNAME=${username}
|
|
254
|
+
OPENGROK_VERIFY_SSL=${verify_ssl}
|
|
255
|
+
EOF
|
|
256
|
+
chmod 644 "$CONFIG_FILE"
|
|
257
|
+
echo " ✓ Saved config: $CONFIG_FILE"
|
|
258
|
+
|
|
259
|
+
echo ""
|
|
260
|
+
echo "╔═══════════════════════════════════════════════════════╗"
|
|
261
|
+
echo "║ Setup complete! Add opengrok to your MCP client. ║"
|
|
262
|
+
echo "╚═══════════════════════════════════════════════════════╝"
|
|
263
|
+
echo ""
|
|
264
|
+
echo "Example client config (Claude Code, Cursor, Windsurf):"
|
|
265
|
+
echo ""
|
|
266
|
+
echo ' { "mcpServers": { "opengrok": { "command": "'"${BASH_SOURCE[0]}"'" } } }'
|
|
267
|
+
echo ""
|
|
268
|
+
echo " For OpenCode (opencode.ai):"
|
|
269
|
+
echo ' { "mcp": { "opengrok": { "type": "local", "command": ["'"${BASH_SOURCE[0]}"'"] } } }'
|
|
270
|
+
echo ""
|
|
271
|
+
echo "See MCP_CLIENTS.md for all client configurations."
|
|
272
|
+
echo ""
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
276
|
+
# Server mode: silent credential resolution, then exec the server
|
|
277
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
278
|
+
|
|
279
|
+
_run_server() {
|
|
280
|
+
# Verify the binary exists
|
|
281
|
+
if [[ ! -x "$OPENGROK_BIN" ]]; then
|
|
282
|
+
echo "opengrok-mcp: binary not found or not executable: $OPENGROK_BIN" >&2
|
|
283
|
+
echo "Set OPENGROK_BIN or ensure opengrok-mcp is in the same directory." >&2
|
|
284
|
+
exit 1
|
|
285
|
+
fi
|
|
286
|
+
|
|
287
|
+
# Load non-secret config (URL, username, SSL verify)
|
|
288
|
+
_load_file_vars "$CONFIG_FILE"
|
|
289
|
+
|
|
290
|
+
# ── Credential resolution chain ────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
# 1. Already in environment → use it (backward compat / CI override)
|
|
293
|
+
if [[ -n "${OPENGROK_PASSWORD:-}" ]]; then
|
|
294
|
+
exec "$OPENGROK_BIN" "$@"
|
|
295
|
+
fi
|
|
296
|
+
|
|
297
|
+
# 2. OS Keychain
|
|
298
|
+
local pw=""
|
|
299
|
+
case "$(uname -s)" in
|
|
300
|
+
Darwin)
|
|
301
|
+
pw="$(_macos_keychain_read "${OPENGROK_USERNAME:-$USER}")" || pw=""
|
|
302
|
+
if [[ -n "$pw" ]]; then
|
|
303
|
+
export OPENGROK_PASSWORD="$pw"
|
|
304
|
+
exec "$OPENGROK_BIN" "$@"
|
|
305
|
+
fi
|
|
306
|
+
;;
|
|
307
|
+
Linux)
|
|
308
|
+
if _secrettool_available 2>/dev/null; then
|
|
309
|
+
pw="$(_secrettool_read "${OPENGROK_USERNAME:-$USER}")" || pw=""
|
|
310
|
+
if [[ -n "$pw" ]]; then
|
|
311
|
+
export OPENGROK_PASSWORD="$pw"
|
|
312
|
+
exec "$OPENGROK_BIN" "$@"
|
|
313
|
+
fi
|
|
314
|
+
fi
|
|
315
|
+
;;
|
|
316
|
+
esac
|
|
317
|
+
|
|
318
|
+
# 3. Encrypted credential file (migrate legacy unsalted credentials if needed)
|
|
319
|
+
_migrate_credentials
|
|
320
|
+
if [[ -f "$ENC_FILE" ]]; then
|
|
321
|
+
if command -v openssl >/dev/null 2>&1; then
|
|
322
|
+
pw="$(decrypt_credentials "$ENC_FILE")" || pw=""
|
|
323
|
+
if [[ -n "$pw" ]]; then
|
|
324
|
+
export OPENGROK_PASSWORD="$pw"
|
|
325
|
+
exec "$OPENGROK_BIN" "$@"
|
|
326
|
+
fi
|
|
327
|
+
fi
|
|
328
|
+
fi
|
|
329
|
+
|
|
330
|
+
# 4. Plaintext .env fallback
|
|
331
|
+
if [[ -f "$DOTENV_FILE" ]]; then
|
|
332
|
+
# Warn if .env file is older than 30 days
|
|
333
|
+
local file_age_days=0
|
|
334
|
+
if command -v stat >/dev/null 2>&1; then
|
|
335
|
+
local mod_epoch
|
|
336
|
+
mod_epoch="$(stat -c %Y "$DOTENV_FILE" 2>/dev/null || stat -f %m "$DOTENV_FILE" 2>/dev/null || echo 0)"
|
|
337
|
+
if [[ "$mod_epoch" -gt 0 ]]; then
|
|
338
|
+
file_age_days=$(( ($(date +%s) - mod_epoch) / 86400 ))
|
|
339
|
+
fi
|
|
340
|
+
fi
|
|
341
|
+
if [[ "$file_age_days" -ge 30 ]]; then
|
|
342
|
+
echo "opengrok-mcp: ⚠ Plaintext .env file is ${file_age_days} days old. Consider running --setup with openssl installed." >&2
|
|
343
|
+
fi
|
|
344
|
+
_load_file_vars "$DOTENV_FILE"
|
|
345
|
+
if [[ -n "${OPENGROK_PASSWORD:-}" ]]; then
|
|
346
|
+
exec "$OPENGROK_BIN" "$@"
|
|
347
|
+
fi
|
|
348
|
+
fi
|
|
349
|
+
|
|
350
|
+
# 5. Nothing found — fail with clear message
|
|
351
|
+
echo "opengrok-mcp: No credentials found." >&2
|
|
352
|
+
echo "Run once in your terminal to set up credentials:" >&2
|
|
353
|
+
echo " ${BASH_SOURCE[0]} --setup" >&2
|
|
354
|
+
exit 1
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
358
|
+
# Entry point
|
|
359
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
360
|
+
|
|
361
|
+
case "${1:-}" in
|
|
362
|
+
--setup|-s)
|
|
363
|
+
_run_setup
|
|
364
|
+
;;
|
|
365
|
+
--version|-v)
|
|
366
|
+
exec "$OPENGROK_BIN" --version
|
|
367
|
+
;;
|
|
368
|
+
--help|-h)
|
|
369
|
+
echo "Usage: $(basename "${BASH_SOURCE[0]}") [--setup | --version | --help]"
|
|
370
|
+
echo ""
|
|
371
|
+
echo " --setup Interactive credential setup (run once in your terminal)"
|
|
372
|
+
echo " --version Print server version and exit"
|
|
373
|
+
echo " --help Show this help"
|
|
374
|
+
echo " (no args) Resolve credentials and start the MCP server (spawned by client)"
|
|
375
|
+
;;
|
|
376
|
+
*)
|
|
377
|
+
_run_server "$@"
|
|
378
|
+
;;
|
|
379
|
+
esac
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Packages the standalone MCP server binary into platform-specific archives
|
|
4
|
+
* for distribution via GitHub Releases.
|
|
5
|
+
*
|
|
6
|
+
* Usage: node scripts/package-server.js
|
|
7
|
+
*
|
|
8
|
+
* Outputs (in project root):
|
|
9
|
+
* opengrok-mcp-{version}-linux.tar.gz
|
|
10
|
+
* opengrok-mcp-{version}-darwin.tar.gz
|
|
11
|
+
* opengrok-mcp-{version}-win.zip
|
|
12
|
+
*
|
|
13
|
+
* Each archive contains:
|
|
14
|
+
* opengrok-mcp <- the binary
|
|
15
|
+
* opengrok-mcp-wrapper.sh <- Linux/macOS credential wrapper
|
|
16
|
+
* opengrok-mcp-wrapper.cmd <- Windows .cmd launcher
|
|
17
|
+
* opengrok-mcp-wrapper.ps1 <- Windows PowerShell credential wrapper
|
|
18
|
+
* MCP_CLIENTS.md <- setup and client config guide
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const { execSync } = require('child_process');
|
|
24
|
+
const { version } = require('../package.json');
|
|
25
|
+
|
|
26
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
27
|
+
const OUT_DIR = path.join(ROOT, 'out', 'server');
|
|
28
|
+
const SCRIPTS_DIR = path.join(ROOT, 'scripts');
|
|
29
|
+
|
|
30
|
+
const SERVER_BIN = path.join(OUT_DIR, 'main.js');
|
|
31
|
+
const WRAPPER_SH = path.join(SCRIPTS_DIR, 'opengrok-mcp-wrapper.sh');
|
|
32
|
+
const WRAPPER_CMD = path.join(SCRIPTS_DIR, 'opengrok-mcp-wrapper.cmd');
|
|
33
|
+
const WRAPPER_PS1 = path.join(SCRIPTS_DIR, 'opengrok-mcp-wrapper.ps1');
|
|
34
|
+
const MCP_CLIENTS_MD = path.join(ROOT, 'MCP_CLIENTS.md');
|
|
35
|
+
|
|
36
|
+
// Verify required files exist
|
|
37
|
+
for (const [label, file] of [
|
|
38
|
+
['server binary', SERVER_BIN],
|
|
39
|
+
['wrapper .sh', WRAPPER_SH],
|
|
40
|
+
['wrapper .cmd', WRAPPER_CMD],
|
|
41
|
+
['wrapper .ps1', WRAPPER_PS1],
|
|
42
|
+
['MCP_CLIENTS.md', MCP_CLIENTS_MD],
|
|
43
|
+
]) {
|
|
44
|
+
if (!fs.existsSync(file)) {
|
|
45
|
+
console.error(`Missing required file [${label}]: ${file}`);
|
|
46
|
+
console.error('Run npm run compile before packaging.');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Create a staging directory
|
|
52
|
+
const STAGE = path.join(ROOT, 'out', '_package_stage');
|
|
53
|
+
if (fs.existsSync(STAGE)) fs.rmSync(STAGE, { recursive: true });
|
|
54
|
+
fs.mkdirSync(STAGE, { recursive: true });
|
|
55
|
+
|
|
56
|
+
// Copy files into staging dir
|
|
57
|
+
fs.copyFileSync(SERVER_BIN, path.join(STAGE, 'opengrok-mcp'));
|
|
58
|
+
fs.copyFileSync(WRAPPER_SH, path.join(STAGE, 'opengrok-mcp-wrapper.sh'));
|
|
59
|
+
fs.copyFileSync(WRAPPER_CMD, path.join(STAGE, 'opengrok-mcp-wrapper.cmd'));
|
|
60
|
+
fs.copyFileSync(WRAPPER_PS1, path.join(STAGE, 'opengrok-mcp-wrapper.ps1'));
|
|
61
|
+
fs.copyFileSync(MCP_CLIENTS_MD, path.join(STAGE, 'MCP_CLIENTS.md'));
|
|
62
|
+
|
|
63
|
+
// Set executable bit on Unix files (no-op on Windows, harmless)
|
|
64
|
+
try {
|
|
65
|
+
fs.chmodSync(path.join(STAGE, 'opengrok-mcp'), 0o755);
|
|
66
|
+
fs.chmodSync(path.join(STAGE, 'opengrok-mcp-wrapper.sh'), 0o755);
|
|
67
|
+
} catch {
|
|
68
|
+
// On Windows chmod may fail — acceptable
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Build archives
|
|
72
|
+
const archives = [
|
|
73
|
+
{
|
|
74
|
+
name: `opengrok-mcp-server-${version}-linux.tar.gz`,
|
|
75
|
+
cmd: `tar -czf "${ROOT}/opengrok-mcp-server-${version}-linux.tar.gz" -C "${STAGE}" .`,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: `opengrok-mcp-server-${version}-darwin.tar.gz`,
|
|
79
|
+
cmd: `tar -czf "${ROOT}/opengrok-mcp-server-${version}-darwin.tar.gz" -C "${STAGE}" .`,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: `opengrok-mcp-server-${version}-win.zip`,
|
|
83
|
+
build: buildWindowsZip,
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
for (const archive of archives) {
|
|
88
|
+
const outPath = path.join(ROOT, archive.name);
|
|
89
|
+
console.log(`Building ${archive.name}...`);
|
|
90
|
+
try {
|
|
91
|
+
if (archive.cmd) {
|
|
92
|
+
execSync(archive.cmd, { stdio: 'inherit' });
|
|
93
|
+
} else if (archive.build) {
|
|
94
|
+
archive.build(outPath);
|
|
95
|
+
}
|
|
96
|
+
const stat = fs.statSync(outPath);
|
|
97
|
+
console.log(` → ${outPath} (${(stat.size / 1024).toFixed(1)} KB)`);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error(` ✗ Failed to build ${archive.name}:`, err.message);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Cleanup staging dir
|
|
105
|
+
fs.rmSync(STAGE, { recursive: true });
|
|
106
|
+
|
|
107
|
+
console.log('\nPackaging complete. Archives ready for release:');
|
|
108
|
+
for (const archive of archives) {
|
|
109
|
+
console.log(` ${archive.name}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Build a .zip archive for Windows using Node's built-in zlib (no external deps).
|
|
114
|
+
* Falls back to PowerShell's Compress-Archive on Windows if available.
|
|
115
|
+
*/
|
|
116
|
+
function buildWindowsZip(outPath) {
|
|
117
|
+
// Try PowerShell first (available on Windows CI runners)
|
|
118
|
+
try {
|
|
119
|
+
execSync(
|
|
120
|
+
`powershell -NoProfile -Command "Compress-Archive -Path '${STAGE}\\*' -DestinationPath '${outPath}' -Force"`,
|
|
121
|
+
{ stdio: 'inherit' }
|
|
122
|
+
);
|
|
123
|
+
return;
|
|
124
|
+
} catch {
|
|
125
|
+
// Not on Windows — fall through to zip CLI
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Try zip CLI (available on Linux/macOS CI)
|
|
129
|
+
try {
|
|
130
|
+
execSync(`cd "${STAGE}" && zip -r "${outPath}" .`, { stdio: 'inherit' });
|
|
131
|
+
return;
|
|
132
|
+
} catch {
|
|
133
|
+
// zip not available
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Minimal fallback: create a tar.gz with .zip extension hint in the name
|
|
137
|
+
// (not ideal but won't break the release pipeline)
|
|
138
|
+
console.warn(' Warning: zip not available. Creating tar.gz for Windows archive.');
|
|
139
|
+
execSync(`tar -czf "${outPath}" -C "${STAGE}" .`, { stdio: 'inherit' });
|
|
140
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# OpenGrok MCP Release Script
|
|
2
|
+
# Usage: .\scripts\release.ps1 -Version [patch|minor|major] [-Dry]
|
|
3
|
+
|
|
4
|
+
param(
|
|
5
|
+
[Parameter(Mandatory=$true)]
|
|
6
|
+
[ValidateSet('patch', 'minor', 'major')]
|
|
7
|
+
[string]$Version,
|
|
8
|
+
|
|
9
|
+
[switch]$Dry = $false
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
$ErrorActionPreference = "Stop"
|
|
13
|
+
|
|
14
|
+
# Colors for output
|
|
15
|
+
function Write-Step([string]$Message) {
|
|
16
|
+
Write-Host "===> $Message" -ForegroundColor Cyan
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function Write-Success([string]$Message) {
|
|
20
|
+
Write-Host "Success: $Message" -ForegroundColor Green
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function Write-Fail([string]$Message) {
|
|
24
|
+
Write-Host "Error: $Message" -ForegroundColor Red
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Check if working directory is clean
|
|
28
|
+
Write-Step "Checking Git status..."
|
|
29
|
+
$gitStatus = git status --porcelain
|
|
30
|
+
if ($gitStatus -and !$Dry) {
|
|
31
|
+
Write-Fail "Working directory has uncommitted changes. Please commit or stash them first."
|
|
32
|
+
exit 1
|
|
33
|
+
}
|
|
34
|
+
Write-Success "Working directory is clean"
|
|
35
|
+
|
|
36
|
+
# Get current version
|
|
37
|
+
Write-Step "Reading current version..."
|
|
38
|
+
$packageJson = Get-Content "package.json" -Raw | ConvertFrom-Json
|
|
39
|
+
$currentVersion = $packageJson.version
|
|
40
|
+
Write-Host "Current version: $currentVersion" -ForegroundColor Yellow
|
|
41
|
+
|
|
42
|
+
# Bump version
|
|
43
|
+
Write-Step "Bumping $Version version..."
|
|
44
|
+
if ($Dry) {
|
|
45
|
+
Write-Host "[DRY RUN] Would run: npm version $Version --no-git-tag-version" -ForegroundColor Yellow
|
|
46
|
+
} else {
|
|
47
|
+
npm version $Version --no-git-tag-version
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Get new version
|
|
51
|
+
$packageJson = Get-Content "package.json" -Raw | ConvertFrom-Json
|
|
52
|
+
$newVersion = $packageJson.version
|
|
53
|
+
Write-Success "New version: $newVersion"
|
|
54
|
+
|
|
55
|
+
# Run tests
|
|
56
|
+
Write-Step "Running tests..."
|
|
57
|
+
if ($Dry) {
|
|
58
|
+
Write-Host "[DRY RUN] Would run: npm test" -ForegroundColor Yellow
|
|
59
|
+
} else {
|
|
60
|
+
npm test
|
|
61
|
+
if ($LASTEXITCODE -ne 0) {
|
|
62
|
+
Write-Fail "Tests failed. Aborting release."
|
|
63
|
+
exit 1
|
|
64
|
+
}
|
|
65
|
+
Write-Success "Tests passed"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Build extension
|
|
69
|
+
Write-Step "Building extension..."
|
|
70
|
+
if ($Dry) {
|
|
71
|
+
Write-Host "[DRY RUN] Would run: npm run compile" -ForegroundColor Yellow
|
|
72
|
+
} else {
|
|
73
|
+
npm run compile
|
|
74
|
+
Write-Success "Extension compiled"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Package standalone server archives
|
|
78
|
+
Write-Step "Packaging standalone server archives..."
|
|
79
|
+
if ($Dry) {
|
|
80
|
+
Write-Host "[DRY RUN] Would run: npm run package-server" -ForegroundColor Yellow
|
|
81
|
+
} else {
|
|
82
|
+
npm run package-server
|
|
83
|
+
Write-Success "Standalone archives created"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Package VSIX
|
|
87
|
+
Write-Step "Packaging VSIX..."
|
|
88
|
+
if ($Dry) {
|
|
89
|
+
Write-Host "[DRY RUN] Would run: npm run vsix" -ForegroundColor Yellow
|
|
90
|
+
} else {
|
|
91
|
+
npm run vsix
|
|
92
|
+
Write-Success "VSIX packaged: opengrok-mcp-$newVersion.vsix"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Git commit and tag
|
|
96
|
+
Write-Step "Creating Git commit and tag..."
|
|
97
|
+
if ($Dry) {
|
|
98
|
+
Write-Host "[DRY RUN] Would run:" -ForegroundColor Yellow
|
|
99
|
+
Write-Host " git add package.json CHANGELOG.md" -ForegroundColor Yellow
|
|
100
|
+
Write-Host " git commit -m 'chore: release v$newVersion'" -ForegroundColor Yellow
|
|
101
|
+
Write-Host " git tag v$newVersion" -ForegroundColor Yellow
|
|
102
|
+
} else {
|
|
103
|
+
git add package.json CHANGELOG.md
|
|
104
|
+
git commit -m "chore: release v$newVersion"
|
|
105
|
+
git tag "v$newVersion"
|
|
106
|
+
Write-Success "Created commit and tag v$newVersion"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Summary
|
|
110
|
+
Write-Host ""
|
|
111
|
+
Write-Host "========================================" -ForegroundColor Green
|
|
112
|
+
Write-Host "Release Summary" -ForegroundColor Green
|
|
113
|
+
Write-Host "========================================" -ForegroundColor Green
|
|
114
|
+
Write-Host "Version: $currentVersion -> $newVersion"
|
|
115
|
+
Write-Host "Tag: v$newVersion"
|
|
116
|
+
Write-Host "VSIX: opengrok-mcp-$newVersion.vsix"
|
|
117
|
+
Write-Host "Archives: opengrok-mcp-$newVersion-linux.tar.gz, -darwin.tar.gz, -win.zip"
|
|
118
|
+
Write-Host ""
|
|
119
|
+
|
|
120
|
+
if ($Dry) {
|
|
121
|
+
Write-Host "[DRY RUN] No changes were made" -ForegroundColor Yellow
|
|
122
|
+
} else {
|
|
123
|
+
Write-Host "Next steps:" -ForegroundColor Cyan
|
|
124
|
+
Write-Host "1. Review the changes: git log -1"
|
|
125
|
+
Write-Host "2. Push to GitHub:"
|
|
126
|
+
Write-Host " git push origin $((git branch --show-current))"
|
|
127
|
+
Write-Host " git push origin v$newVersion"
|
|
128
|
+
Write-Host ""
|
|
129
|
+
Write-Host "3. GitHub Actions will automatically:" -ForegroundColor Yellow
|
|
130
|
+
Write-Host " - Run tests on the tag"
|
|
131
|
+
Write-Host " - Build the VSIX + standalone server archives"
|
|
132
|
+
Write-Host " - Create a GitHub Release"
|
|
133
|
+
Write-Host " - Attach the VSIX and platform archives as downloadable artifacts"
|
|
134
|
+
Write-Host ""
|
|
135
|
+
Write-Host "4. Check the release at:" -ForegroundColor Cyan
|
|
136
|
+
Write-Host " https://github.com/IcyHot09/opengrok-mcp-server/releases"
|
|
137
|
+
}
|