@@ -4,156 +4,157 @@ import urllib.request;
44import urllib .parse ;
55import json ;
66import urllib .error ;
7+ import from http { HTTPMethod }
78
89import 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