Skip to content

Commit bd3ff52

Browse files
authored
Refactor MCP proxy implementation
2 parents 65b060b + e64807d commit bd3ff52

2 files changed

Lines changed: 142 additions & 141 deletions

File tree

hooks/useMcpClient.cl.jac

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
"""useMcpClient – hook that performs JSON-RPC calls against an MCP server.
22
3-
Routes all calls through the backend mcp_proxy walker which handles CORS.
3+
Routes all calls through the backend /api/mcp-proxy endpoint which handles CORS.
44
"""
55

6-
sv import from ..main { mcp_proxy }
76
cl import from ..store.mcp_store { useMcpStore }
87

98
cl {
@@ -47,7 +46,7 @@ cl {
4746
addHistoryEntry = useMcpStore(lambda(s: Any) -> Any { return s.addHistoryEntry; });
4847
updateHistoryEntry = useMcpStore(lambda(s: Any) -> Any { return s.updateHistoryEntry; });
4948

50-
# Core send function — routes through backend proxy walker
49+
# Core send function — routes through backend proxy function
5150
def sendRpc(connection: Any, method: Any, params: Any = None) -> Any {
5251
async def _exec -> Any {
5352
rpcReq = buildRpcRequest(method, params);
@@ -74,18 +73,19 @@ cl {
7473
addHistoryEntry(histEntry);
7574

7675
try {
77-
result = await root spawn mcp_proxy(
78-
url=connection.url,
79-
body=rpcReq,
80-
headers=authHeaders,
81-
transport=connection.transport or "streamable-http"
82-
);
83-
84-
resp = None;
85-
if result.reports and len(result.reports) > 0 {
86-
inner = result.reports[0];
87-
resp = inner[0] if inner and len(inner) > 0 else inner;
88-
}
76+
fetchResp = await fetch("/mcp-proxy", {
77+
"method": "POST",
78+
"headers": {"Content-Type": "application/json"},
79+
"body": JSON.stringify({
80+
"url": connection.url,
81+
"body": rpcReq,
82+
"headers": authHeaders,
83+
"transport": connection.transport or "streamable-http"
84+
})
85+
});
86+
rawJson = await fetchResp.json();
87+
# @restspec wraps in TransportResponse: {status, data: {result: ...}}
88+
resp = rawJson.data.result if rawJson and rawJson.data and rawJson.data.result else None;
8989
durationMs = Date.now() - startMs;
9090

9191
if not resp or not resp.ok {

main.jac

Lines changed: 127 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -4,156 +4,157 @@ import urllib.request;
44
import urllib.parse;
55
import json;
66
import urllib.error;
7+
import from http { HTTPMethod }
78

89
import from mcp_server_launcher { _mcp_process }
910

1011
# ──────────────────────────────────────────────
11-
# Backend: CORS proxy walker for MCP requests
12+
# Backend: CORS proxy function for MCP requests
1213
# Forwards JSON-RPC calls to any MCP server URL
14+
# Uses @restspec to avoid /walker/* path (blocked by ALB)
1315
# ──────────────────────────────────────────────
1416

15-
walker:pub mcp_proxy {
16-
has url: str,
17-
body: dict = {},
18-
headers: dict = {},
19-
transport: str = "streamable-http";
20-
21-
can proxy with Root entry {
22-
23-
request_headers = {
24-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36",
25-
"Content-Type": "application/json",
26-
"Accept": "application/json, text/event-stream"
27-
};
28-
29-
for key in self.headers {
30-
request_headers[key] = self.headers[key];
31-
}
17+
@restspec(method=HTTPMethod.POST, path="/mcp-proxy")
18+
def:pub mcp_proxy(
19+
url: str,
20+
body: dict = {},
21+
headers: dict = {},
22+
transport: str = "streamable-http"
23+
) -> dict {
24+
25+
request_headers = {
26+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36",
27+
"Content-Type": "application/json",
28+
"Accept": "application/json, text/event-stream"
29+
};
30+
31+
for key in headers {
32+
request_headers[key] = headers[key];
33+
}
3234

33-
print(f"[mcp_proxy] method={self.body.get('method','?')} transport={self.transport}");
34-
35-
# Use explicit transport mode instead of URL auto-detection
36-
parsed_url = urllib.parse.urlparse(self.url);
37-
is_sse = self.transport == "sse";
38-
39-
post_url = self.url;
40-
sse_error = None;
41-
42-
if is_sse {
43-
# SSE Transport: GET the /sse endpoint first to receive the session POST URL
44-
try {
45-
sse_headers = {
46-
"Accept": "text/event-stream",
47-
"Cache-Control": "no-cache",
48-
"User-Agent": request_headers["User-Agent"]
49-
};
50-
for key in self.headers {
51-
sse_headers[key] = self.headers[key];
52-
}
53-
get_req = urllib.request.Request(self.url, headers=sse_headers, method="GET");
54-
endpoint_url = None;
55-
with urllib.request.urlopen(get_req, timeout=10) as sse_conn {
56-
event_type = "";
57-
for _ in range(200) {
58-
raw_line = sse_conn.readline();
59-
if not raw_line { break; }
60-
line = raw_line.decode("utf-8", errors="replace").rstrip("\r\n");
61-
if line.startswith("event:") {
62-
event_type = line[6:].strip();
63-
} elif line.startswith("data:") and event_type == "endpoint" {
64-
path = line[5:].strip();
65-
if path {
66-
endpoint_url = path if path.startswith("http") else f"{parsed_url.scheme}://{parsed_url.netloc}{path}";
67-
break;
68-
}
69-
} elif line == "" {
70-
event_type = "";
35+
print(f"[mcp_proxy] method={body.get('method','?')} transport={transport}");
36+
37+
# Use explicit transport mode instead of URL auto-detection
38+
parsed_url = urllib.parse.urlparse(url);
39+
is_sse = transport == "sse";
40+
41+
post_url = url;
42+
sse_error = None;
43+
44+
if is_sse {
45+
# SSE Transport: GET the /sse endpoint first to receive the session POST URL
46+
try {
47+
sse_headers = {
48+
"Accept": "text/event-stream",
49+
"Cache-Control": "no-cache",
50+
"User-Agent": request_headers["User-Agent"]
51+
};
52+
for key in headers {
53+
sse_headers[key] = headers[key];
54+
}
55+
get_req = urllib.request.Request(url, headers=sse_headers, method="GET");
56+
endpoint_url = None;
57+
with urllib.request.urlopen(get_req, timeout=10) as sse_conn {
58+
event_type = "";
59+
for _ in range(200) {
60+
raw_line = sse_conn.readline();
61+
if not raw_line { break; }
62+
line = raw_line.decode("utf-8", errors="replace").rstrip("\r\n");
63+
if line.startswith("event:") {
64+
event_type = line[6:].strip();
65+
} elif line.startswith("data:") and event_type == "endpoint" {
66+
path = line[5:].strip();
67+
if path {
68+
endpoint_url = path if path.startswith("http") else f"{parsed_url.scheme}://{parsed_url.netloc}{path}";
69+
break;
7170
}
71+
} elif line == "" {
72+
event_type = "";
7273
}
7374
}
74-
if endpoint_url {
75-
post_url = endpoint_url;
76-
} else {
77-
sse_error = "SSE server did not send an endpoint URL. Try the /mcp endpoint instead.";
78-
}
79-
} except Exception as e {
80-
sse_error = f"SSE session setup failed: {str(e)}. Try the /mcp endpoint instead.";
8175
}
76+
if endpoint_url {
77+
post_url = endpoint_url;
78+
} else {
79+
sse_error = "SSE server did not send an endpoint URL. Try the /mcp endpoint instead.";
80+
}
81+
} except Exception as e {
82+
sse_error = f"SSE session setup failed: {str(e)}. Try the /mcp endpoint instead.";
8283
}
84+
}
8385

84-
if sse_error {
85-
report {"ok": False, "status": 0, "error": sse_error, "body": ""};
86-
} else {
87-
req_body = json.dumps(self.body).encode("utf-8");
88-
req = urllib.request.Request(post_url, data=req_body, headers=request_headers, method="POST");
89-
90-
try {
91-
with urllib.request.urlopen(req, timeout=30) as resp {
92-
content_type = resp.headers.get("Content-Type", "");
93-
94-
# Capture session ID from response headers (MCP Streamable HTTP transport)
95-
session_id = resp.headers.get("mcp-session-id", "");
96-
97-
if "text/event-stream" in content_type {
98-
# Read SSE line-by-line; stop after first complete event (blank line)
99-
events = [];
100-
try {
101-
for _ in range(1000) {
102-
raw_line = resp.readline();
103-
if not raw_line { break; }
104-
line = raw_line.decode("utf-8", errors="replace").rstrip("\r\n");
105-
if line.startswith("data:") {
106-
data_str = line[5:].strip();
107-
if data_str and data_str != "[DONE]" {
108-
try {
109-
events.append(json.loads(data_str));
110-
} except Exception {
111-
events.append({"raw": data_str});
112-
}
113-
}
114-
}
115-
# SSE event separator — stop once we have data
116-
if events and line == "" {
117-
break;
86+
if sse_error {
87+
return {"ok": False, "status": 0, "error": sse_error, "body": ""};
88+
}
89+
90+
req_body = json.dumps(body).encode("utf-8");
91+
req = urllib.request.Request(post_url, data=req_body, headers=request_headers, method="POST");
92+
93+
try {
94+
with urllib.request.urlopen(req, timeout=30) as resp {
95+
content_type = resp.headers.get("Content-Type", "");
96+
97+
# Capture session ID from response headers (MCP Streamable HTTP transport)
98+
session_id = resp.headers.get("mcp-session-id", "");
99+
100+
if "text/event-stream" in content_type {
101+
# Read SSE line-by-line; stop after first complete event (blank line)
102+
events = [];
103+
try {
104+
for _ in range(1000) {
105+
raw_line = resp.readline();
106+
if not raw_line { break; }
107+
line = raw_line.decode("utf-8", errors="replace").rstrip("\r\n");
108+
if line.startswith("data:") {
109+
data_str = line[5:].strip();
110+
if data_str and data_str != "[DONE]" {
111+
try {
112+
events.append(json.loads(data_str));
113+
} except Exception {
114+
events.append({"raw": data_str});
118115
}
119116
}
120-
} except Exception as loop_e {
121-
print(f"[mcp_proxy] readline loop exception: {loop_e}");
122-
}
123-
124-
if events {
125-
report {"ok": True, "streaming": True, "events": events, "sessionId": session_id};
126-
} else {
127-
report {"ok": False, "status": 0, "error": "SSE stream returned no events.", "body": ""};
128117
}
129-
} else {
130-
raw = resp.read().decode("utf-8");
131-
if raw.strip() {
132-
try {
133-
report {"ok": True, "streaming": False, "data": json.loads(raw), "sessionId": session_id};
134-
} except Exception {
135-
report {"ok": True, "streaming": False, "data": {"raw": raw}, "sessionId": session_id};
136-
}
137-
} else {
138-
report {"ok": False, "status": 0, "error": "Server returned empty body.", "body": ""};
118+
# SSE event separator — stop once we have data
119+
if events and line == "" {
120+
break;
139121
}
140122
}
123+
} except Exception as loop_e {
124+
print(f"[mcp_proxy] readline loop exception: {loop_e}");
125+
}
126+
127+
if events {
128+
return {"ok": True, "streaming": True, "events": events, "sessionId": session_id};
129+
} else {
130+
return {"ok": False, "status": 0, "error": "SSE stream returned no events.", "body": ""};
141131
}
142-
} except urllib.error.HTTPError as e {
143-
if e.code in (301, 302, 307, 308) {
144-
redirect_url = e.headers.get("Location", "");
145-
hint = f" Try: {redirect_url}" if redirect_url else " Try adding a trailing slash to the URL.";
146-
report {"ok": False, "status": e.code, "error": f"Server redirected the request.{hint}", "body": ""};
132+
} else {
133+
raw = resp.read().decode("utf-8");
134+
if raw.strip() {
135+
try {
136+
return {"ok": True, "streaming": False, "data": json.loads(raw), "sessionId": session_id};
137+
} except Exception {
138+
return {"ok": True, "streaming": False, "data": {"raw": raw}, "sessionId": session_id};
139+
}
147140
} else {
148-
err_body = e.read().decode("utf-8") if e.fp else "";
149-
report {"ok": False, "status": e.code, "error": str(e), "body": err_body};
141+
return {"ok": False, "status": 0, "error": "Server returned empty body.", "body": ""};
150142
}
151-
} except urllib.error.URLError as e {
152-
report {"ok": False, "status": 0, "error": str(e.reason), "body": ""};
153-
} except Exception as e {
154-
report {"ok": False, "status": 0, "error": str(e), "body": ""};
155143
}
156144
}
145+
} except urllib.error.HTTPError as e {
146+
if e.code in (301, 302, 307, 308) {
147+
redirect_url = e.headers.get("Location", "");
148+
hint = f" Try: {redirect_url}" if redirect_url else " Try adding a trailing slash to the URL.";
149+
return {"ok": False, "status": e.code, "error": f"Server redirected the request.{hint}", "body": ""};
150+
} else {
151+
err_body = e.read().decode("utf-8") if e.fp else "";
152+
return {"ok": False, "status": e.code, "error": str(e), "body": err_body};
153+
}
154+
} except urllib.error.URLError as e {
155+
return {"ok": False, "status": 0, "error": str(e.reason), "body": ""};
156+
} except Exception as e {
157+
return {"ok": False, "status": 0, "error": str(e), "body": ""};
157158
}
158159
}
159160

0 commit comments

Comments
 (0)