@marsnme/mcp-gateway 0.1.6 → 0.1.8
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/README.md +21 -1
- package/deploy/phase2/pre_deploy_schema_gate.sh +186 -0
- package/deploy/phase3/smoke_gate.sh +84 -8
- package/package.json +13 -4
- package/server.mjs +39 -2
package/README.md
CHANGED
|
@@ -39,7 +39,11 @@ After startup:
|
|
|
39
39
|
|
|
40
40
|
## Common optional environment variables
|
|
41
41
|
- `PORT`
|
|
42
|
-
- Overrides
|
|
42
|
+
- Overrides the HTTP port used by the gateway
|
|
43
|
+
- If omitted, the gateway resolves a profile-based default:
|
|
44
|
+
- `coco` → `18790`
|
|
45
|
+
- `toto` → `18791`
|
|
46
|
+
- other profile IDs → deterministic port in `20000-29999`
|
|
43
47
|
- `MCP_REQUIRE_BEARER`
|
|
44
48
|
- Set `true` to require `Authorization: Bearer <token>` on MCP calls
|
|
45
49
|
- `MCP_OAUTH_ENABLED`
|
|
@@ -60,6 +64,22 @@ curl -sS http://127.0.0.1:18790/mcp \
|
|
|
60
64
|
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
|
|
61
65
|
```
|
|
62
66
|
|
|
67
|
+
## Deployment safety gate (before restart)
|
|
68
|
+
For upgrades, run database migrations with an explicit DDL-capable role and gate schema compatibility before restarting services.
|
|
69
|
+
|
|
70
|
+
Recommended role guidance:
|
|
71
|
+
- Use a role that can execute DDL on target schemas.
|
|
72
|
+
- On Supabase-hosted Postgres this is typically `supabase_admin` (not `postgres`).
|
|
73
|
+
|
|
74
|
+
Schema gate command:
|
|
75
|
+
```bash
|
|
76
|
+
bash deploy/phase2/pre_deploy_schema_gate.sh \
|
|
77
|
+
--db-url "<postgres://supabase_admin:<password>@<host>:5432/postgres>" \
|
|
78
|
+
--profiles coco,toto \
|
|
79
|
+
--expected-role supabase_admin
|
|
80
|
+
```
|
|
81
|
+
- If gate exits non-zero, stop deployment and do not restart services.
|
|
82
|
+
|
|
63
83
|
## Database setup and full onboarding
|
|
64
84
|
For migrations and full setup details:
|
|
65
85
|
- Repository README: https://github.com/Marsmanleo/MarsNMe/blob/main/README.md
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
DB_URL="${SUPABASE_DB_URL:-}"
|
|
6
|
+
PROFILES="coco,toto"
|
|
7
|
+
EXPECTED_ROLE="${MIGRATION_EXPECTED_ROLE:-supabase_admin}"
|
|
8
|
+
SKIP_ROLE_CHECK="false"
|
|
9
|
+
|
|
10
|
+
usage() {
|
|
11
|
+
cat <<EOF
|
|
12
|
+
usage: $0 --db-url <postgres-url> [--profiles <comma-separated>] [--expected-role <role>] [--skip-role-check]
|
|
13
|
+
|
|
14
|
+
examples:
|
|
15
|
+
$0 --db-url "<postgres-connection-string>"
|
|
16
|
+
$0 --db-url "<postgres-connection-string>" --profiles coco,toto --expected-role supabase_admin
|
|
17
|
+
$0 --db-url "<postgres-connection-string>" --profiles profile-a --expected-role app_owner
|
|
18
|
+
$0 --db-url "<postgres-connection-string>" --profiles profile-a --skip-role-check
|
|
19
|
+
|
|
20
|
+
notes:
|
|
21
|
+
- run this gate before any service restart in deployment flow
|
|
22
|
+
- exit code 0 means schema is compatible with current gateway code
|
|
23
|
+
- exit code 1 means compatibility gate failed (do NOT restart)
|
|
24
|
+
EOF
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
while [[ $# -gt 0 ]]; do
|
|
28
|
+
case "$1" in
|
|
29
|
+
--db-url)
|
|
30
|
+
DB_URL="$2"
|
|
31
|
+
shift 2
|
|
32
|
+
;;
|
|
33
|
+
--profiles)
|
|
34
|
+
PROFILES="$2"
|
|
35
|
+
shift 2
|
|
36
|
+
;;
|
|
37
|
+
--expected-role)
|
|
38
|
+
EXPECTED_ROLE="$2"
|
|
39
|
+
shift 2
|
|
40
|
+
;;
|
|
41
|
+
--skip-role-check)
|
|
42
|
+
SKIP_ROLE_CHECK="true"
|
|
43
|
+
shift
|
|
44
|
+
;;
|
|
45
|
+
-h|--help)
|
|
46
|
+
usage
|
|
47
|
+
exit 0
|
|
48
|
+
;;
|
|
49
|
+
*)
|
|
50
|
+
echo "unknown arg: $1" >&2
|
|
51
|
+
usage >&2
|
|
52
|
+
exit 2
|
|
53
|
+
;;
|
|
54
|
+
esac
|
|
55
|
+
done
|
|
56
|
+
|
|
57
|
+
if ! command -v psql >/dev/null 2>&1; then
|
|
58
|
+
echo "psql not found in PATH; install PostgreSQL client tools first" >&2
|
|
59
|
+
exit 2
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
if [[ -z "${DB_URL}" ]]; then
|
|
63
|
+
echo "--db-url is required (or set SUPABASE_DB_URL env)" >&2
|
|
64
|
+
exit 2
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
escape_sql_literal() {
|
|
68
|
+
printf "%s" "$1" | sed "s/'/''/g"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
quote_ident() {
|
|
72
|
+
local raw="$1"
|
|
73
|
+
printf '"%s"' "${raw//\"/\"\"}"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
run_sql() {
|
|
77
|
+
local sql="$1"
|
|
78
|
+
psql "${DB_URL}" --set ON_ERROR_STOP=1 --no-psqlrc --tuples-only --no-align --quiet -c "${sql}"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
PASS_COUNT=0
|
|
82
|
+
FAIL_COUNT=0
|
|
83
|
+
|
|
84
|
+
log_pass() {
|
|
85
|
+
printf '[PASS] %s\n' "$1"
|
|
86
|
+
PASS_COUNT=$((PASS_COUNT + 1))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
log_fail() {
|
|
90
|
+
printf '[FAIL] %s\n' "$1" >&2
|
|
91
|
+
FAIL_COUNT=$((FAIL_COUNT + 1))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
check_sql() {
|
|
95
|
+
local label="$1"
|
|
96
|
+
local sql="$2"
|
|
97
|
+
local output
|
|
98
|
+
if output="$(run_sql "${sql}" 2>&1)"; then
|
|
99
|
+
log_pass "${label}"
|
|
100
|
+
return
|
|
101
|
+
fi
|
|
102
|
+
output="$(printf '%s' "${output}" | tr '\n' ' ' | sed -E 's/[[:space:]]+/ /g')"
|
|
103
|
+
log_fail "${label} :: ${output}"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
PROFILE_LIST=()
|
|
107
|
+
IFS=',' read -ra RAW_PROFILES <<< "${PROFILES}"
|
|
108
|
+
for raw_profile in "${RAW_PROFILES[@]}"; do
|
|
109
|
+
profile="$(printf '%s' "${raw_profile}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
|
110
|
+
if [[ -z "${profile}" ]]; then
|
|
111
|
+
continue
|
|
112
|
+
fi
|
|
113
|
+
if [[ ! "${profile}" =~ ^[a-z][a-z0-9_-]*$ ]]; then
|
|
114
|
+
echo "invalid profile id: ${profile} (must match ^[a-z][a-z0-9_-]*$)" >&2
|
|
115
|
+
exit 2
|
|
116
|
+
fi
|
|
117
|
+
PROFILE_LIST+=("${profile}")
|
|
118
|
+
done
|
|
119
|
+
|
|
120
|
+
if [[ ${#PROFILE_LIST[@]} -eq 0 ]]; then
|
|
121
|
+
echo "no valid profiles found from --profiles input: ${PROFILES}" >&2
|
|
122
|
+
exit 2
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
CURRENT_ROLE="$(run_sql "SELECT current_user;" | tr -d '[:space:]')"
|
|
126
|
+
if [[ -z "${CURRENT_ROLE}" ]]; then
|
|
127
|
+
echo "failed to resolve current_user from database" >&2
|
|
128
|
+
exit 2
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
printf '[INFO] pre-deploy schema gate start\n'
|
|
132
|
+
printf '[INFO] target profiles=%s\n' "$(IFS=,; printf '%s' "${PROFILE_LIST[*]}")"
|
|
133
|
+
printf '[INFO] connected role=%s\n' "${CURRENT_ROLE}"
|
|
134
|
+
|
|
135
|
+
if [[ "${SKIP_ROLE_CHECK}" == "true" ]]; then
|
|
136
|
+
printf '[INFO] migration role check skipped by --skip-role-check\n'
|
|
137
|
+
elif [[ -n "${EXPECTED_ROLE}" ]]; then
|
|
138
|
+
expected_role_lit="$(escape_sql_literal "${EXPECTED_ROLE}")"
|
|
139
|
+
role_status="$(run_sql "SELECT CASE WHEN current_user = '${expected_role_lit}' OR pg_has_role(current_user, '${expected_role_lit}', 'member') THEN 'ok' ELSE 'mismatch' END;" | tr -d '[:space:]' || true)"
|
|
140
|
+
if [[ "${role_status}" == "ok" ]]; then
|
|
141
|
+
log_pass "migration role check (expected=${EXPECTED_ROLE})"
|
|
142
|
+
else
|
|
143
|
+
log_fail "migration role check failed (expected=${EXPECTED_ROLE}, got=${CURRENT_ROLE})"
|
|
144
|
+
fi
|
|
145
|
+
else
|
|
146
|
+
printf '[INFO] migration role check disabled (empty expected role)\n'
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
for profile in "${PROFILE_LIST[@]}"; do
|
|
150
|
+
profile_lit="$(escape_sql_literal "${profile}")"
|
|
151
|
+
profile_ident="$(quote_ident "${profile}")"
|
|
152
|
+
|
|
153
|
+
check_sql "${profile}.memories required columns" \
|
|
154
|
+
"SELECT id,body,source,session_id,tags,agent_body,environment,promoted,promoted_at,created_at,expires_at FROM ${profile_ident}.memories LIMIT 0;"
|
|
155
|
+
|
|
156
|
+
check_sql "${profile}.marsvault_chunks required columns" \
|
|
157
|
+
"SELECT id,content,source_file,section,body,visibility,tags,type,date,origin,source_memory_id,source_session_id,source_tool,source_user_note,agent_body,environment,created_at,updated_at FROM ${profile_ident}.marsvault_chunks LIMIT 0;"
|
|
158
|
+
|
|
159
|
+
check_sql "${profile}.memories DDL ownership check" \
|
|
160
|
+
"DO \$\$ DECLARE owner_oid oid; BEGIN SELECT c.relowner INTO owner_oid FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = '${profile_lit}' AND c.relname = 'memories' AND c.relkind = 'r'; IF owner_oid IS NULL THEN RAISE EXCEPTION 'table %.% is missing', '${profile_lit}', 'memories'; END IF; IF NOT ((SELECT rolsuper FROM pg_roles WHERE rolname = current_user) OR owner_oid = (SELECT oid FROM pg_roles WHERE rolname = current_user) OR pg_has_role(current_user, owner_oid, 'MEMBER')) THEN RAISE EXCEPTION 'current_user % cannot ALTER %.memories (owner=%)', current_user, '${profile_lit}', pg_get_userbyid(owner_oid); END IF; END \$\$;"
|
|
161
|
+
|
|
162
|
+
check_sql "${profile}.marsvault_chunks DDL ownership check" \
|
|
163
|
+
"DO \$\$ DECLARE owner_oid oid; BEGIN SELECT c.relowner INTO owner_oid FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = '${profile_lit}' AND c.relname = 'marsvault_chunks' AND c.relkind = 'r'; IF owner_oid IS NULL THEN RAISE EXCEPTION 'table %.% is missing', '${profile_lit}', 'marsvault_chunks'; END IF; IF NOT ((SELECT rolsuper FROM pg_roles WHERE rolname = current_user) OR owner_oid = (SELECT oid FROM pg_roles WHERE rolname = current_user) OR pg_has_role(current_user, owner_oid, 'MEMBER')) THEN RAISE EXCEPTION 'current_user % cannot ALTER %.marsvault_chunks (owner=%)', current_user, '${profile_lit}', pg_get_userbyid(owner_oid); END IF; END \$\$;"
|
|
164
|
+
|
|
165
|
+
check_sql "${profile}.search_memories_semantic(v2) exists" \
|
|
166
|
+
"DO \$\$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname = '${profile_lit}' AND p.proname = 'search_memories_semantic' AND p.pronargs = 7) THEN RAISE EXCEPTION 'missing function %.search_memories_semantic(text, integer, text, boolean, text, text, text)', '${profile_lit}'; END IF; END \$\$;"
|
|
167
|
+
|
|
168
|
+
check_sql "${profile}.search_marsvault_chunks_semantic(v2) exists" \
|
|
169
|
+
"DO \$\$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname = '${profile_lit}' AND p.proname = 'search_marsvault_chunks_semantic' AND p.pronargs = 10) THEN RAISE EXCEPTION 'missing function %.search_marsvault_chunks_semantic(text, integer, text, boolean, boolean, boolean, text, text, text, text)', '${profile_lit}'; END IF; END \$\$;"
|
|
170
|
+
done
|
|
171
|
+
|
|
172
|
+
LATEST_MIGRATION_VERSION="$(run_sql "SELECT version FROM supabase_migrations.schema_migrations ORDER BY version DESC LIMIT 1;" 2>/dev/null | tr -d '[:space:]' || true)"
|
|
173
|
+
if [[ -n "${LATEST_MIGRATION_VERSION}" ]]; then
|
|
174
|
+
printf '[INFO] latest schema_migrations.version=%s\n' "${LATEST_MIGRATION_VERSION}"
|
|
175
|
+
else
|
|
176
|
+
printf '[WARN] schema_migrations version not available (table missing or inaccessible)\n' >&2
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
printf '[INFO] pre-deploy schema gate summary: pass=%d fail=%d\n' "${PASS_COUNT}" "${FAIL_COUNT}"
|
|
180
|
+
|
|
181
|
+
if [[ "${FAIL_COUNT}" -gt 0 ]]; then
|
|
182
|
+
printf '[ERROR] schema gate failed; do NOT restart services until all checks pass\n' >&2
|
|
183
|
+
exit 1
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
printf '[OK] schema gate passed; safe to continue deployment restart flow\n'
|
|
@@ -159,23 +159,99 @@ TOTO_TOOL_CALL_JSON="$(mcp_jsonrpc "${TOTO_URL}" '{"jsonrpc":"2.0","id":"toto-li
|
|
|
159
159
|
|
|
160
160
|
COCO_CALL_CHECK="$(CALL_PAYLOAD="${COCO_TOOL_CALL_JSON}" node -e '
|
|
161
161
|
const payload = JSON.parse(process.env.CALL_PAYLOAD || "{}");
|
|
162
|
-
|
|
162
|
+
const clip = (value, max = 800) => {
|
|
163
|
+
const text = String(value ?? "");
|
|
164
|
+
return text.length > max ? text.slice(0, max) + "...(truncated)" : text;
|
|
165
|
+
};
|
|
166
|
+
const extractErrorText = (value) => {
|
|
167
|
+
if (value == null) return "";
|
|
168
|
+
if (typeof value === "string") return value;
|
|
169
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
170
|
+
if (Array.isArray(value)) return value.map(extractErrorText).filter(Boolean).join(" | ");
|
|
171
|
+
if (typeof value === "object") {
|
|
172
|
+
if (typeof value.message === "string" && value.message) return value.message;
|
|
173
|
+
if (typeof value.error === "string" && value.error) return value.error;
|
|
174
|
+
if (value.error) {
|
|
175
|
+
const nested = extractErrorText(value.error);
|
|
176
|
+
if (nested) return nested;
|
|
177
|
+
}
|
|
178
|
+
if (value.details) {
|
|
179
|
+
const details = extractErrorText(value.details);
|
|
180
|
+
if (details) return details;
|
|
181
|
+
}
|
|
182
|
+
return JSON.stringify(value);
|
|
183
|
+
}
|
|
184
|
+
return String(value);
|
|
185
|
+
};
|
|
186
|
+
if (payload.error) throw new Error("tools/call error: " + clip(JSON.stringify(payload.error)));
|
|
163
187
|
const content = payload.result?.content;
|
|
164
188
|
if (!Array.isArray(content) || content.length === 0) throw new Error("tools/call missing content");
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
189
|
+
const textItems = content
|
|
190
|
+
.map((item) => (typeof item?.text === "string" ? item.text.trim() : ""))
|
|
191
|
+
.filter(Boolean);
|
|
192
|
+
if (payload.result?.isError === true) {
|
|
193
|
+
throw new Error("tools/call returned isError=true; detail=" + clip(textItems.join(" | ")));
|
|
194
|
+
}
|
|
195
|
+
const firstText = textItems[0] || "";
|
|
196
|
+
let parsed;
|
|
197
|
+
try {
|
|
198
|
+
parsed = JSON.parse(firstText);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
throw new Error("tools/call first content is not valid JSON: " + clip(firstText));
|
|
201
|
+
}
|
|
202
|
+
if (parsed.ok !== true) {
|
|
203
|
+
const inner = extractErrorText(parsed.error) || extractErrorText(parsed.details) || extractErrorText(parsed.message) || clip(JSON.stringify(parsed));
|
|
204
|
+
throw new Error("list_memories returned ok!=true; detail=" + clip(inner));
|
|
205
|
+
}
|
|
168
206
|
process.stdout.write(JSON.stringify({ count: parsed.count ?? null }));
|
|
169
207
|
')"
|
|
170
208
|
|
|
171
209
|
TOTO_CALL_CHECK="$(CALL_PAYLOAD="${TOTO_TOOL_CALL_JSON}" node -e '
|
|
172
210
|
const payload = JSON.parse(process.env.CALL_PAYLOAD || "{}");
|
|
173
|
-
|
|
211
|
+
const clip = (value, max = 800) => {
|
|
212
|
+
const text = String(value ?? "");
|
|
213
|
+
return text.length > max ? text.slice(0, max) + "...(truncated)" : text;
|
|
214
|
+
};
|
|
215
|
+
const extractErrorText = (value) => {
|
|
216
|
+
if (value == null) return "";
|
|
217
|
+
if (typeof value === "string") return value;
|
|
218
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
219
|
+
if (Array.isArray(value)) return value.map(extractErrorText).filter(Boolean).join(" | ");
|
|
220
|
+
if (typeof value === "object") {
|
|
221
|
+
if (typeof value.message === "string" && value.message) return value.message;
|
|
222
|
+
if (typeof value.error === "string" && value.error) return value.error;
|
|
223
|
+
if (value.error) {
|
|
224
|
+
const nested = extractErrorText(value.error);
|
|
225
|
+
if (nested) return nested;
|
|
226
|
+
}
|
|
227
|
+
if (value.details) {
|
|
228
|
+
const details = extractErrorText(value.details);
|
|
229
|
+
if (details) return details;
|
|
230
|
+
}
|
|
231
|
+
return JSON.stringify(value);
|
|
232
|
+
}
|
|
233
|
+
return String(value);
|
|
234
|
+
};
|
|
235
|
+
if (payload.error) throw new Error("tools/call error: " + clip(JSON.stringify(payload.error)));
|
|
174
236
|
const content = payload.result?.content;
|
|
175
237
|
if (!Array.isArray(content) || content.length === 0) throw new Error("tools/call missing content");
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
238
|
+
const textItems = content
|
|
239
|
+
.map((item) => (typeof item?.text === "string" ? item.text.trim() : ""))
|
|
240
|
+
.filter(Boolean);
|
|
241
|
+
if (payload.result?.isError === true) {
|
|
242
|
+
throw new Error("tools/call returned isError=true; detail=" + clip(textItems.join(" | ")));
|
|
243
|
+
}
|
|
244
|
+
const firstText = textItems[0] || "";
|
|
245
|
+
let parsed;
|
|
246
|
+
try {
|
|
247
|
+
parsed = JSON.parse(firstText);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
throw new Error("tools/call first content is not valid JSON: " + clip(firstText));
|
|
250
|
+
}
|
|
251
|
+
if (parsed.ok !== true) {
|
|
252
|
+
const inner = extractErrorText(parsed.error) || extractErrorText(parsed.details) || extractErrorText(parsed.message) || clip(JSON.stringify(parsed));
|
|
253
|
+
throw new Error("list_memories returned ok!=true; detail=" + clip(inner));
|
|
254
|
+
}
|
|
179
255
|
process.stdout.write(JSON.stringify({ count: parsed.count ?? null }));
|
|
180
256
|
')"
|
|
181
257
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marsnme/mcp-gateway",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"mcpName": "io.github.Marsmanleo/marsnme",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Agent-agnostic, LLM-agnostic memory backend for MCP-compatible tools",
|
|
@@ -14,10 +14,19 @@
|
|
|
14
14
|
"keywords": [
|
|
15
15
|
"mcp",
|
|
16
16
|
"memory",
|
|
17
|
-
"ai",
|
|
18
|
-
"
|
|
17
|
+
"ai-memory",
|
|
18
|
+
"persistent-memory",
|
|
19
|
+
"ai-agent",
|
|
20
|
+
"model-context-protocol",
|
|
21
|
+
"semantic-search",
|
|
22
|
+
"supabase",
|
|
23
|
+
"jina",
|
|
19
24
|
"embedding",
|
|
20
|
-
"
|
|
25
|
+
"typescript",
|
|
26
|
+
"session-memory",
|
|
27
|
+
"cross-session",
|
|
28
|
+
"llm",
|
|
29
|
+
"symbiosis"
|
|
21
30
|
],
|
|
22
31
|
"bin": {
|
|
23
32
|
"marsnme": "server.mjs"
|
package/server.mjs
CHANGED
|
@@ -45,6 +45,19 @@ const PROFILE_CONFIGS = {
|
|
|
45
45
|
memoryIngestFixedTags: ['toto', 'insight']
|
|
46
46
|
}
|
|
47
47
|
};
|
|
48
|
+
const PROFILE_DEFAULT_PORT_RANGE_START = 20000;
|
|
49
|
+
const PROFILE_DEFAULT_PORT_RANGE_SIZE = 10000;
|
|
50
|
+
const PROFILE_LEGACY_DEFAULT_PORTS = {
|
|
51
|
+
coco: 18790,
|
|
52
|
+
toto: 18791
|
|
53
|
+
};
|
|
54
|
+
function resolveProfileDefaultPort(profileId) {
|
|
55
|
+
const legacyPort = PROFILE_LEGACY_DEFAULT_PORTS[profileId];
|
|
56
|
+
if (Number.isInteger(legacyPort)) return legacyPort;
|
|
57
|
+
const hash = crypto.createHash('sha256').update(profileId).digest();
|
|
58
|
+
const slot = hash.readUInt16BE(0) % PROFILE_DEFAULT_PORT_RANGE_SIZE;
|
|
59
|
+
return PROFILE_DEFAULT_PORT_RANGE_START + slot;
|
|
60
|
+
}
|
|
48
61
|
const PROFILE_ID_PATTERN = /^[a-z][a-z0-9_-]*$/;
|
|
49
62
|
function buildProfileConfig(profileId) {
|
|
50
63
|
const legacyProfile = PROFILE_CONFIGS[profileId];
|
|
@@ -53,7 +66,7 @@ function buildProfileConfig(profileId) {
|
|
|
53
66
|
...PROFILE_CONFIGS.coco,
|
|
54
67
|
schema: profileId,
|
|
55
68
|
displayName: profileId,
|
|
56
|
-
defaultPort:
|
|
69
|
+
defaultPort: resolveProfileDefaultPort(profileId),
|
|
57
70
|
gatewayDir: `${profileId}-mcp-gateway`,
|
|
58
71
|
publicHostSuffix: `${profileId}-mcp.marsgroup.asia`,
|
|
59
72
|
recallBodyEnum: [profileId, 'system'],
|
|
@@ -151,7 +164,12 @@ function setMemorySourceWhitelist(nextSources) {
|
|
|
151
164
|
}
|
|
152
165
|
setMemorySourceWhitelist(buildMemorySourceListForMode());
|
|
153
166
|
|
|
154
|
-
const
|
|
167
|
+
const PORT_RAW = String(process.env.PORT || '').trim();
|
|
168
|
+
const PORT_SOURCE = PORT_RAW ? 'env' : 'profile-default';
|
|
169
|
+
const PORT = Number.parseInt(PORT_RAW || String(PROFILE.defaultPort), 10);
|
|
170
|
+
if (!Number.isInteger(PORT) || PORT < 1 || PORT > 65535) {
|
|
171
|
+
throw new Error(`Invalid PORT: ${PORT_RAW || String(PORT)}. Must be an integer between 1 and 65535.`);
|
|
172
|
+
}
|
|
155
173
|
const SUPABASE_BASE_URL = process.env.SUPABASE_BASE_URL || 'http://127.0.0.1:8100';
|
|
156
174
|
const OAUTH_ENABLED = process.env.MCP_OAUTH_ENABLED !== 'false';
|
|
157
175
|
const REQUIRE_BEARER = process.env.MCP_REQUIRE_BEARER === 'true';
|
|
@@ -4336,8 +4354,27 @@ const server = http.createServer(async (req, res) => {
|
|
|
4336
4354
|
}
|
|
4337
4355
|
});
|
|
4338
4356
|
|
|
4357
|
+
server.on('error', (error) => {
|
|
4358
|
+
if (error?.code === 'EADDRINUSE') {
|
|
4359
|
+
const hint =
|
|
4360
|
+
PORT_SOURCE === 'env'
|
|
4361
|
+
? `Set PORT to an unused value (current PORT=${PORT}).`
|
|
4362
|
+
: `Set PORT to an unused value or use another MCP_PROFILE (profile=${MCP_PROFILE}, default_port=${PROFILE.defaultPort}).`;
|
|
4363
|
+
console.error(`[fatal] ${SERVER_NAME} cannot start: port ${PORT} is already in use. ${hint}`);
|
|
4364
|
+
process.exit(1);
|
|
4365
|
+
return;
|
|
4366
|
+
}
|
|
4367
|
+
console.error(
|
|
4368
|
+
`[fatal] ${SERVER_NAME} failed to start: ${String(error?.message || error)}`
|
|
4369
|
+
);
|
|
4370
|
+
process.exit(1);
|
|
4371
|
+
});
|
|
4372
|
+
|
|
4339
4373
|
server.listen(PORT, '0.0.0.0', () => {
|
|
4340
4374
|
console.log(`${SERVER_NAME} listening on 0.0.0.0:${PORT}`);
|
|
4375
|
+
console.log(
|
|
4376
|
+
`[config] profile=${MCP_PROFILE} resolved_port=${PORT} source=${PORT_SOURCE} profile_default_port=${PROFILE.defaultPort}`
|
|
4377
|
+
);
|
|
4341
4378
|
console.log(`[config] source_mode=${SOURCE_MODE}`);
|
|
4342
4379
|
if (SOURCE_MODE === 'registry') {
|
|
4343
4380
|
void reloadSourceRegistryCache()
|