@malloy-publisher/server 0.0.77 → 0.0.78

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.
@@ -12,8 +12,8 @@
12
12
  href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap"
13
13
  />
14
14
  <title>Malloy Publisher</title>
15
- <script type="module" crossorigin src="/assets/index--hbjs-yl.js"></script>
16
- <link rel="stylesheet" crossorigin href="/assets/index-C127D7lT.css">
15
+ <script type="module" crossorigin src="/assets/index-DQrKrnRW.js"></script>
16
+ <link rel="stylesheet" crossorigin href="/assets/index-BKC-ikb3.css">
17
17
  </head>
18
18
  <body>
19
19
  <div id="root"></div>
@@ -3,6 +3,7 @@ import sys
3
3
  import json
4
4
  import urllib.request
5
5
  import urllib.parse
6
+ import urllib.error
6
7
  import logging
7
8
  import time
8
9
  import signal
@@ -18,81 +19,126 @@ logging.basicConfig(
18
19
  class ImprovedMalloyMCPBridge:
19
20
  def __init__(self):
20
21
  self.mcp_url = "http://localhost:4040/mcp"
21
-
22
+ # No tool name mapping needed - server already uses underscore format
23
+
22
24
  # Set stdin/stdout to line buffered mode for better responsiveness
23
- sys.stdin.reconfigure(line_buffering=True)
24
- sys.stdout.reconfigure(line_buffering=True)
25
-
25
+ try:
26
+ sys.stdin.reconfigure(line_buffering=True)
27
+ sys.stdout.reconfigure(line_buffering=True)
28
+ except AttributeError:
29
+ # reconfigure not available in older Python versions
30
+ pass
31
+
26
32
  # Set up signal handlers for graceful shutdown
27
33
  signal.signal(signal.SIGTERM, self.signal_handler)
28
34
  signal.signal(signal.SIGINT, self.signal_handler)
29
-
30
35
  logging.info("Bridge initialized with improved stdio handling")
31
-
36
+
32
37
  def signal_handler(self, signum, frame):
33
38
  logging.info(f"Received signal {signum}, shutting down gracefully")
34
39
  sys.exit(0)
35
-
36
- def parse_sse_response(self, response_text: str, request_id: Optional[Any] = None) -> Dict[str, Any]:
40
+
41
+ def parse_sse_response(self, response, request_id: Optional[Any] = None) -> Dict[str, Any]:
37
42
  """Parse Server-Sent Events response format with improved error handling"""
38
- lines = response_text.strip().split('\n')
39
- data_line = None
40
-
41
- # Look for data line in SSE format
42
- for line in lines:
43
- if line.startswith('data: '):
44
- data_line = line[6:] # Remove 'data: ' prefix
45
- break
43
+ logging.debug(f"Starting SSE parsing for request {request_id}")
46
44
 
47
- # If no SSE format, try parsing the entire response as JSON
48
- if not data_line:
49
- data_line = response_text.strip()
50
-
51
- if data_line:
52
- try:
53
- parsed_data = json.loads(data_line)
54
- # Ensure the response has a proper ID
55
- if 'id' not in parsed_data or parsed_data['id'] is None:
56
- parsed_data['id'] = request_id
57
-
58
- logging.debug(f"Successfully parsed response for request {request_id}")
59
- return parsed_data
60
-
61
- except json.JSONDecodeError as e:
62
- logging.error(f"JSON decode error for request {request_id}: {e}")
45
+ try:
46
+ # Read response with size limit to prevent memory issues
47
+ max_size = 1024 * 1024 # 1MB limit
48
+ response_text = ""
49
+ bytes_read = 0
50
+
51
+ # Read response in chunks to avoid memory issues
52
+ while True:
53
+ chunk = response.read(8192) # 8KB chunks
54
+ if not chunk:
55
+ break
56
+ bytes_read += len(chunk)
57
+ if bytes_read > max_size:
58
+ logging.warning(f"Response too large ({bytes_read} bytes), truncating")
59
+ break
60
+ response_text += chunk.decode('utf-8')
61
+
62
+ logging.debug(f"Read {bytes_read} bytes from SSE response")
63
+
64
+ lines = response_text.strip().split('\n')
65
+ data_line = None
66
+
67
+ # Look for data line in SSE format
68
+ for line in lines:
69
+ if line.startswith('data: '):
70
+ data_line = line[6:] # Remove 'data: ' prefix
71
+ break
72
+
73
+ # If no SSE format, try parsing the entire response as JSON
74
+ if not data_line:
75
+ data_line = response_text.strip()
76
+
77
+ if data_line:
78
+ try:
79
+ parsed_data = json.loads(data_line)
80
+ # Ensure the response has a proper ID
81
+ if 'id' not in parsed_data or parsed_data['id'] is None:
82
+ parsed_data['id'] = request_id
83
+
84
+ logging.debug(f"Successfully parsed response for request {request_id}")
85
+ return parsed_data
86
+
87
+ except json.JSONDecodeError as e:
88
+ logging.error(f"JSON decode error for request {request_id}: {e}")
89
+ logging.error(f"Raw data that failed to parse: {data_line[:200]}...")
90
+ return {
91
+ "jsonrpc": "2.0",
92
+ "error": {
93
+ "code": -32603,
94
+ "message": f"Failed to parse response: {str(e)}"
95
+ },
96
+ "id": request_id
97
+ }
98
+ else:
99
+ logging.error(f"No data found in response for request {request_id}")
63
100
  return {
64
101
  "jsonrpc": "2.0",
65
102
  "error": {
66
103
  "code": -32603,
67
- "message": f"Failed to parse response: {str(e)}"
104
+ "message": "No data found in response"
68
105
  },
69
106
  "id": request_id
70
107
  }
71
- else:
72
- logging.error(f"No data found in response for request {request_id}")
108
+
109
+ except Exception as e:
110
+ logging.error(f"Error reading SSE response for request {request_id}: {e}")
73
111
  return {
74
112
  "jsonrpc": "2.0",
75
113
  "error": {
76
114
  "code": -32603,
77
- "message": "No data found in response"
115
+ "message": f"SSE parsing error: {str(e)}"
78
116
  },
79
117
  "id": request_id
80
118
  }
81
-
119
+
82
120
  def send_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
83
121
  """Send a JSON-RPC request to the Malloy MCP endpoint with improved timeout handling"""
84
122
  request_id = request.get("id")
85
123
  method = request.get("method", "unknown")
86
124
  start_time = time.time()
87
-
125
+
88
126
  logging.debug(f"Sending request {request_id} ({method}) to Malloy server")
89
127
 
128
+ # Log ALL method calls for debugging
129
+ logging.info(f"METHOD CALL - Request {request_id}: method='{method}'")
130
+ if method == "tools/call":
131
+ tool_name = request.get("params", {}).get("name", "unknown")
132
+ logging.info(f"TOOL CALL - Request {request_id}: tool_name='{tool_name}'")
133
+ logging.info(f"FULL REQUEST - {json.dumps(request)}")
134
+
90
135
  try:
91
136
  # Ensure the request has proper structure
92
137
  if "jsonrpc" not in request:
93
138
  request["jsonrpc"] = "2.0"
94
-
95
- # Prepare the request with shorter timeout for responsiveness
139
+
140
+ # Prepare the request with longer timeout for tools/list
141
+ timeout = 10 if method == "tools/list" else 3
96
142
  data = json.dumps(request).encode('utf-8')
97
143
  req = urllib.request.Request(
98
144
  self.mcp_url,
@@ -103,15 +149,44 @@ class ImprovedMalloyMCPBridge:
103
149
  'Connection': 'close' # Don't keep connections open
104
150
  }
105
151
  )
152
+
153
+ logging.debug(f"Using timeout of {timeout}s for method {method}")
106
154
 
107
- with urllib.request.urlopen(req, timeout=60) as response:
108
- response_text = response.read().decode('utf-8')
109
- parsed_response = self.parse_sse_response(response_text, request_id)
155
+ with urllib.request.urlopen(req, timeout=timeout) as response:
156
+ # Handle SSE response directly from the response object
157
+ content_type = response.headers.get('Content-Type', '')
158
+
159
+ if 'text/event-stream' in content_type:
160
+ logging.debug("Handling SSE response")
161
+ parsed_response = self.parse_sse_response(response, request_id)
162
+ else:
163
+ logging.debug("Handling JSON response")
164
+ response_text = response.read().decode('utf-8')
165
+ try:
166
+ parsed_response = json.loads(response_text)
167
+ if 'id' not in parsed_response or parsed_response['id'] is None:
168
+ parsed_response['id'] = request_id
169
+ except json.JSONDecodeError as e:
170
+ logging.error(f"Failed to parse JSON response: {e}")
171
+ parsed_response = {
172
+ "jsonrpc": "2.0",
173
+ "error": {
174
+ "code": -32603,
175
+ "message": f"Invalid JSON response: {str(e)}"
176
+ },
177
+ "id": request_id
178
+ }
179
+
180
+ # Log server response for debugging
181
+ if "error" in parsed_response:
182
+ logging.error(f"SERVER ERROR - Request {request_id} ({method}): {parsed_response}")
183
+ elif method == "tools/call":
184
+ logging.info(f"TOOL CALL - Server response: {str(parsed_response)[:500]}...")
110
185
 
111
186
  elapsed = time.time() - start_time
112
187
  logging.debug(f"Request {request_id} completed in {elapsed:.2f}s")
113
188
  return parsed_response
114
-
189
+
115
190
  except urllib.error.HTTPError as e:
116
191
  elapsed = time.time() - start_time
117
192
  error_message = f"HTTP {e.code}: {e.reason}"
@@ -120,7 +195,7 @@ class ImprovedMalloyMCPBridge:
120
195
  error_message += f" - {error_body}"
121
196
  except:
122
197
  pass
123
-
198
+
124
199
  logging.error(f"HTTP error for request {request_id} after {elapsed:.2f}s: {error_message}")
125
200
  return {
126
201
  "jsonrpc": "2.0",
@@ -130,7 +205,7 @@ class ImprovedMalloyMCPBridge:
130
205
  },
131
206
  "id": request_id
132
207
  }
133
-
208
+
134
209
  except urllib.error.URLError as e:
135
210
  elapsed = time.time() - start_time
136
211
  error_msg = f"Connection error: {str(e)}"
@@ -143,7 +218,7 @@ class ImprovedMalloyMCPBridge:
143
218
  },
144
219
  "id": request_id
145
220
  }
146
-
221
+
147
222
  except Exception as e:
148
223
  elapsed = time.time() - start_time
149
224
  error_msg = f"Unexpected error: {str(e)}"
@@ -156,51 +231,56 @@ class ImprovedMalloyMCPBridge:
156
231
  },
157
232
  "id": request_id
158
233
  }
159
-
234
+
160
235
  def safe_print(self, data):
161
236
  """Print with improved error handling and immediate flushing"""
162
237
  try:
163
238
  output = json.dumps(data)
164
- print(output)
165
- sys.stdout.flush() # Force immediate flush
239
+ print(output, flush=True) # Use flush=True for immediate output
166
240
  logging.debug(f"Successfully sent response: {len(output)} chars")
167
-
241
+
168
242
  except BrokenPipeError:
169
243
  logging.error("Broken pipe error - client disconnected")
170
244
  sys.exit(0)
171
-
245
+
172
246
  except Exception as e:
173
247
  logging.error(f"Print error: {e}")
174
248
  # Don't exit on print errors, just log them
175
-
249
+
176
250
  def process_request(self, line: str) -> None:
177
251
  """Process a single request line with improved error handling"""
178
252
  try:
179
253
  request = json.loads(line)
180
254
  request_id = request.get("id", "unknown")
181
255
  method = request.get("method", "unknown")
182
-
256
+
183
257
  logging.debug(f"Processing request {request_id}: {method}")
184
-
258
+
185
259
  # Validate required fields
186
260
  if not isinstance(request, dict):
187
261
  raise ValueError("Request must be a JSON object")
188
-
262
+
189
263
  if "method" not in request:
190
264
  raise ValueError("Request must have a 'method' field")
191
-
265
+
266
+ # Handle notifications/initialized locally (Malloy server doesn't support it)
267
+ if method == "notifications/initialized":
268
+ logging.info(f"Handling notifications/initialized locally for request {request_id}")
269
+ # For notifications, we don't send a response (notifications are one-way)
270
+ return
271
+
192
272
  # Ensure ID is present and valid
193
273
  if "id" not in request:
194
274
  request["id"] = 1 # Default ID
195
275
  elif request["id"] is None:
196
276
  request["id"] = 1 # Replace null with default
197
-
277
+
198
278
  # Send request and get response
199
279
  response = self.send_request(request)
200
-
280
+
201
281
  # Send response immediately
202
282
  self.safe_print(response)
203
-
283
+
204
284
  except json.JSONDecodeError as e:
205
285
  logging.error(f"JSON parse error: {e}")
206
286
  error_response = {
@@ -212,7 +292,7 @@ class ImprovedMalloyMCPBridge:
212
292
  "id": None
213
293
  }
214
294
  self.safe_print(error_response)
215
-
295
+
216
296
  except ValueError as e:
217
297
  logging.error(f"Request validation error: {e}")
218
298
  error_response = {
@@ -224,7 +304,7 @@ class ImprovedMalloyMCPBridge:
224
304
  "id": None
225
305
  }
226
306
  self.safe_print(error_response)
227
-
307
+
228
308
  except Exception as e:
229
309
  logging.error(f"Unexpected error processing request: {e}")
230
310
  error_response = {
@@ -236,33 +316,33 @@ class ImprovedMalloyMCPBridge:
236
316
  "id": None
237
317
  }
238
318
  self.safe_print(error_response)
239
-
319
+
240
320
  def run(self):
241
321
  """Main loop with improved stdin handling"""
242
322
  logging.info("Starting main processing loop")
243
-
323
+
244
324
  try:
245
325
  # Process stdin line by line with immediate handling
246
326
  for line in sys.stdin:
247
327
  line = line.strip()
248
328
  if not line:
249
329
  continue
250
-
330
+
251
331
  # Process each request immediately
252
332
  self.process_request(line)
253
-
333
+
254
334
  except KeyboardInterrupt:
255
335
  logging.info("Received keyboard interrupt, shutting down")
256
336
  sys.exit(0)
257
-
337
+
258
338
  except BrokenPipeError:
259
339
  logging.info("Broken pipe detected, client disconnected")
260
340
  sys.exit(0)
261
-
341
+
262
342
  except Exception as e:
263
343
  logging.error(f"Fatal error in main loop: {e}")
264
344
  sys.exit(1)
265
-
345
+
266
346
  logging.info("Main loop completed")
267
347
 
268
348
  if __name__ == "__main__":
package/malloy_mcp.dxt CHANGED
Binary file
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@malloy-publisher/server",
3
3
  "description": "Malloy Publisher Server",
4
- "version": "0.0.77",
4
+ "version": "0.0.78",
5
5
  "main": "dist/server.js",
6
6
  "bin": {
7
7
  "malloy-publisher": "dist/server.js"
@@ -1 +0,0 @@
1
- import{r,i as y,j as b}from"./index--hbjs-yl.js";function w({result:o,height:E,isFillElement:c,onSizeChange:f,onDrill:p}){const e=r.useRef(null),[d,h]=r.useState(!1),[m,x]=r.useState(!1),[g,v]=r.useState(!1),n=r.useRef(null),t=r.useRef(null);if(r.useLayoutEffect(()=>{if(e.current&&o&&!m){x(!0),n.current||(n.current=new Promise(s=>{t.current=s}));const u=new y.MalloyRenderer({onClick:p}).createViz();for(;e.current.firstChild;)e.current.removeChild(e.current.firstChild);const l=new MutationObserver(s=>{for(const i of s)if(i.type==="childList"&&i.addedNodes.length>0&&Array.from(i.addedNodes).some(a=>a.nodeType===Node.ELEMENT_NODE)){l.disconnect(),setTimeout(()=>{h(!0),t.current&&(t.current(),t.current=null,n.current=null)},50);break}});if(e.current){l.observe(e.current,{childList:!0,subtree:!0,characterData:!0});try{u.setResult(JSON.parse(o)),u.render(e.current)}catch(s){console.error("Error rendering visualization:",s),l.disconnect(),h(!0),t.current&&(t.current(),t.current=null,n.current=null)}}}},[o,p,m]),r.useEffect(()=>{h(!1),x(!1),n.current=null,t.current=null},[o]),m&&!d&&n.current)throw n.current;return r.useEffect(()=>{if(!e.current||!d)return;const u=e.current,l=()=>{if(u){const a=u.offsetHeight;a>0?f&&f(a):c&&u.firstChild&&(u.firstChild.offsetHeight==0?c(!0):c(!1))}},s=setTimeout(l,100);let i=null;return g||(i=new MutationObserver(l),i.observe(u,{childList:!0,subtree:!0,attributes:!0})),()=>{clearTimeout(s),i?.disconnect()}},[f,o,c,d]),b.jsx("div",{ref:e,style:{width:"100%",height:E?`${E}px`:"100%"}})}export{w as default};