Skip to content

Commit 8501021

Browse files
committed
fix(metamcp): recreate stale transport server records
1 parent 85561bd commit 8501021

5 files changed

Lines changed: 91 additions & 9 deletions

File tree

charts/metamcp/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ apiVersion: v2
22
name: metamcp
33
description: MetaMCP aggregator Helm chart for Kubernetes
44
type: application
5-
version: 0.3.6
5+
version: 0.3.7
66
appVersion: "2.4.22"
77
icon: https://icoretech.github.io/helm/charts/metamcp/logo.png
88
keywords:

charts/metamcp/scripts/provision.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from urllib.parse import urlparse
66
import base64
77

8+
from provision_lib import normalize_server_type, server_needs_recreate
9+
810
def log(msg):
911
print(f"[provision] {msg}", flush=True)
1012

@@ -370,11 +372,9 @@ def delete_server(name, servers_by_name):
370372
for s in servers:
371373
if not (s.get('enabled', True)):
372374
continue
373-
name = s.get('name'); st = (s.get('type','SSE') or 'SSE').upper()
375+
name = s.get('name'); st = normalize_server_type(s.get('type'))
374376
if not name:
375377
continue
376-
if st in ('SSE','STREAMABLE'):
377-
st = 'SSE' if st=='SSE' else 'STREAMABLE_HTTP'
378378
body = {'name': name, 'type': st}
379379
# Pre-compute desired fields for STDIO so we can also update existing servers
380380
desired_cmd = None
@@ -435,9 +435,14 @@ def delete_server(name, servers_by_name):
435435

436436
# If server exists and updates are allowed, patch safe fields
437437
if name in srv_map:
438-
if UPDATE_EXISTING and st in ('SSE','STREAMABLE_HTTP'):
438+
current = srv_info.get(name, {})
439+
if UPDATE_EXISTING and server_needs_recreate(current, st):
440+
log(f"server transport cleanup: recreating {name}")
441+
delete_server(name, srv_info)
442+
srv_map.pop(name, None)
443+
srv_info.pop(name, None)
444+
if UPDATE_EXISTING and name in srv_map and st in ('SSE','STREAMABLE_HTTP'):
439445
try:
440-
current = srv_info.get(name, {})
441446
patch = {'uuid': current.get('uuid'), 'name': name, 'type': st}
442447
# Always include required field for HTTP/SSE: url
443448
if desired_url:
@@ -454,9 +459,8 @@ def delete_server(name, servers_by_name):
454459
log(f"WARN server update {name} -> {r.status_code}: {r.text[:160]}")
455460
except Exception:
456461
pass
457-
elif UPDATE_EXISTING and st == 'STDIO':
462+
elif UPDATE_EXISTING and name in srv_map and st == 'STDIO':
458463
try:
459-
current = srv_info.get(name, {})
460464
patch = {'uuid': current.get('uuid'), 'name': name, 'type': st}
461465
# Always include required field for STDIO: command
462466
if desired_cmd is not None:
@@ -473,7 +477,8 @@ def delete_server(name, servers_by_name):
473477
log(f"WARN server update {name} -> {r.status_code}: {r.text[:160]}")
474478
except Exception:
475479
pass
476-
continue
480+
if name in srv_map:
481+
continue
477482
if st == 'STDIO':
478483
if desired_cmd is not None:
479484
body['command'] = desired_cmd
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
def normalize_server_type(raw_type):
2+
server_type = (raw_type or "SSE").upper()
3+
if server_type in ("SSE", "STREAMABLE"):
4+
return "SSE" if server_type == "SSE" else "STREAMABLE_HTTP"
5+
return server_type
6+
7+
8+
def _has_value(value):
9+
return value not in (None, "", [], {})
10+
11+
12+
def server_needs_recreate(current, desired_type):
13+
current_type = normalize_server_type(current.get("type"))
14+
if current_type != desired_type:
15+
return True
16+
17+
if desired_type in ("SSE", "STREAMABLE_HTTP"):
18+
return any(
19+
_has_value(current.get(key))
20+
for key in ("command", "args", "env")
21+
)
22+
23+
if desired_type == "STDIO":
24+
return any(
25+
_has_value(current.get(key))
26+
for key in ("url", "headers", "bearerToken")
27+
)
28+
29+
return False

charts/metamcp/templates/provision-config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ data:
3030
# No separate workloads.json; serviceBase is included per server above when deploy specs exist
3131
provision.py: |
3232
{{ (.Files.Get "scripts/provision.py") | indent 4 }}
33+
provision_lib.py: |
34+
{{ (.Files.Get "scripts/provision_lib.py") | indent 4 }}
3335
{{- end }}
3436

3537
---
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import importlib.util
2+
import pathlib
3+
import unittest
4+
5+
6+
MODULE_PATH = pathlib.Path(__file__).resolve().parent.parent / "scripts" / "provision_lib.py"
7+
SPEC = importlib.util.spec_from_file_location("provision_lib", MODULE_PATH)
8+
MODULE = importlib.util.module_from_spec(SPEC)
9+
assert SPEC.loader is not None
10+
SPEC.loader.exec_module(MODULE)
11+
12+
13+
class ProvisionLibTests(unittest.TestCase):
14+
def test_recreates_when_transport_changes(self):
15+
current = {
16+
"type": "STDIO",
17+
"command": "npx",
18+
"args": ["-y", "@icoretech/warden-mcp@latest", "--stdio"],
19+
"env": {"BW_HOST": "https://secret-manager.icorete.ch"},
20+
}
21+
22+
self.assertTrue(MODULE.server_needs_recreate(current, "STREAMABLE_HTTP"))
23+
24+
def test_recreates_when_http_server_keeps_stale_stdio_fields(self):
25+
current = {
26+
"type": "STREAMABLE_HTTP",
27+
"url": "http://example.svc.cluster.local:3005/sse?v=2",
28+
"command": "npx",
29+
"args": ["-y", "@icoretech/warden-mcp@latest", "--stdio"],
30+
"env": {"BW_HOST": "https://secret-manager.icorete.ch"},
31+
}
32+
33+
self.assertTrue(MODULE.server_needs_recreate(current, "STREAMABLE_HTTP"))
34+
35+
def test_keeps_clean_http_server_in_place(self):
36+
current = {
37+
"type": "STREAMABLE_HTTP",
38+
"url": "http://example.svc.cluster.local:3005/sse?v=2",
39+
"headers": {"X-Test": "1"},
40+
}
41+
42+
self.assertFalse(MODULE.server_needs_recreate(current, "STREAMABLE_HTTP"))
43+
44+
45+
if __name__ == "__main__":
46+
unittest.main()

0 commit comments

Comments
 (0)