@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.
- package/dist/app/assets/RenderedResult-BAZuT25g-etaZB-YF.js +2 -0
- package/dist/app/assets/index-BKC-ikb3.css +1 -0
- package/dist/app/assets/index-BsMlh93-.js +210 -0
- package/dist/app/assets/index-BsXDwHRS.js +671 -0
- package/dist/app/assets/index-DQrKrnRW.js +432 -0
- package/dist/app/assets/index.umd-DaHgfkHn.js +2078 -0
- package/dist/app/index.html +2 -2
- package/dxt/malloy_bridge.py +149 -69
- package/malloy_mcp.dxt +0 -0
- package/package.json +1 -1
- package/dist/app/assets/RenderedResult-DXlUZo0k-lc5JU6CQ.js +0 -1
- package/dist/app/assets/index--hbjs-yl.js +0 -3414
- package/dist/app/assets/index-C127D7lT.css +0 -1
package/dist/app/index.html
CHANGED
|
@@ -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
|
|
16
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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>
|
package/dxt/malloy_bridge.py
CHANGED
|
@@ -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
|
-
|
|
24
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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":
|
|
104
|
+
"message": "No data found in response"
|
|
68
105
|
},
|
|
69
106
|
"id": request_id
|
|
70
107
|
}
|
|
71
|
-
|
|
72
|
-
|
|
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": "
|
|
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
|
|
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=
|
|
108
|
-
|
|
109
|
-
|
|
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 +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};
|