griptape-nodes 0.52.0__py3-none-any.whl → 0.52.1__py3-none-any.whl

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.
griptape_nodes/app/api.py CHANGED
@@ -141,17 +141,6 @@ async def _delete_static_file(file_path: str, static_directory: Annotated[Path,
141
141
  return {"message": f"File {file_path} deleted successfully"}
142
142
 
143
143
 
144
- @app.post("/engines/request")
145
- async def _create_event(request: Request) -> None:
146
- """Create event using centralized event utilities."""
147
- from .app import _process_api_event
148
-
149
- body = await request.json()
150
-
151
- # Use centralized event processing
152
- await _process_api_event(body)
153
-
154
-
155
144
  def _setup_app(static_directory: Path) -> None:
156
145
  """Setup FastAPI app with middleware and static files."""
157
146
  global static_dir # noqa: PLW0603
@@ -179,7 +168,7 @@ def _setup_app(static_directory: Path) -> None:
179
168
  )
180
169
 
181
170
 
182
- def start_api(static_directory: Path) -> None:
171
+ def start_static_server(static_directory: Path) -> None:
183
172
  """Run uvicorn server synchronously using uvicorn.run."""
184
173
  # Setup the FastAPI app
185
174
  _setup_app(static_directory)
griptape_nodes/app/app.py CHANGED
@@ -1,12 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import contextlib
5
- import contextvars
6
4
  import json
7
5
  import logging
8
6
  import os
9
7
  import sys
8
+ import threading
9
+ from dataclasses import dataclass
10
10
  from pathlib import Path
11
11
  from typing import Any
12
12
  from urllib.parse import urljoin
@@ -18,6 +18,8 @@ from rich.panel import Panel
18
18
  from websockets.asyncio.client import connect
19
19
  from websockets.exceptions import ConnectionClosed, WebSocketException
20
20
 
21
+ from griptape_nodes.app.api import start_static_server
22
+ from griptape_nodes.mcp_server.server import start_mcp_server
21
23
  from griptape_nodes.retained_mode.events import app_events, execution_events
22
24
 
23
25
  # This import is necessary to register all events, even if not technically used
@@ -36,11 +38,41 @@ from griptape_nodes.retained_mode.events.base_events import (
36
38
  from griptape_nodes.retained_mode.events.logger_events import LogHandlerEvent
37
39
  from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
38
40
 
39
- # Context variable for WebSocket connection - avoids global state
40
- ws_connection_context: contextvars.ContextVar[Any | None] = contextvars.ContextVar("ws_connection", default=None)
41
41
 
42
- # Event to signal when WebSocket connection is ready
43
- ws_ready_event = asyncio.Event()
42
+ # WebSocket thread communication message types
43
+ @dataclass
44
+ class WebSocketMessage:
45
+ """Message to send via WebSocket."""
46
+
47
+ event_type: str
48
+ payload: str
49
+ topic: str | None = None
50
+
51
+
52
+ @dataclass
53
+ class SubscribeCommand:
54
+ """Command to subscribe to a topic."""
55
+
56
+ topic: str
57
+
58
+
59
+ @dataclass
60
+ class UnsubscribeCommand:
61
+ """Command to unsubscribe from a topic."""
62
+
63
+ topic: str
64
+
65
+
66
+ # WebSocket outgoing queue for messages and commands.
67
+ # Appears to be fine to create outside event loop
68
+ # https://discuss.python.org/t/can-asyncio-queue-be-safely-created-outside-of-the-event-loop-thread/49215/8
69
+ ws_outgoing_queue: asyncio.Queue = asyncio.Queue()
70
+
71
+ # Background WebSocket event loop reference for cross-thread communication
72
+ websocket_event_loop: asyncio.AbstractEventLoop | None = None
73
+
74
+ # Threading event to signal when websocket_event_loop is ready
75
+ websocket_event_loop_ready = threading.Event()
44
76
 
45
77
 
46
78
  # Whether to enable the static server
@@ -95,62 +127,84 @@ async def astart_app() -> None:
95
127
  """New async app entry point."""
96
128
  api_key = _ensure_api_key()
97
129
 
130
+ # Initialize event queue in main thread
98
131
  griptape_nodes.EventManager().initialize_queue()
99
132
 
100
- # Create shared context for all tasks to inherit WebSocket connection
101
- shared_context = contextvars.copy_context()
133
+ # Get main loop reference
134
+ main_loop = asyncio.get_running_loop()
102
135
 
103
136
  try:
104
- # We need to run the servers in a separate thread otherwise
105
- # blocking requests to them in the main thread would deadlock the event loop.
106
- server_tasks = []
137
+ # Start MCP server in daemon thread
138
+ threading.Thread(target=start_mcp_server, args=(api_key,), daemon=True, name="mcp-server").start()
107
139
 
108
- # Start MCP server in thread
109
- server_tasks.append(asyncio.to_thread(_run_mcp_server_sync, api_key))
110
-
111
- # Start static server in thread if enabled
140
+ # Start static server in daemon thread if enabled
112
141
  if STATIC_SERVER_ENABLED:
113
142
  static_dir = _build_static_dir()
114
- server_tasks.append(asyncio.to_thread(_run_static_server_sync, static_dir))
143
+ threading.Thread(target=start_static_server, args=(static_dir,), daemon=True, name="static-server").start()
115
144
 
116
- # Run main event loop tasks
117
- main_tasks = [
118
- _listen_for_api_requests(api_key),
119
- _process_event_queue(),
120
- ]
145
+ # Start WebSocket tasks in daemon thread
146
+ threading.Thread(
147
+ target=_start_websocket_connection, args=(api_key, main_loop), daemon=True, name="websocket-tasks"
148
+ ).start()
121
149
 
122
- # Combine server tasks and main tasks
123
- all_tasks = server_tasks + main_tasks
150
+ # Run event processing on main thread
151
+ await _process_event_queue()
124
152
 
125
- async with asyncio.TaskGroup() as tg:
126
- for task in all_tasks:
127
- # Context is supposed to be copied automatically, but it isn't working for some reason so we do it manually here
128
- tg.create_task(task, context=shared_context)
129
153
  except Exception as e:
130
154
  logger.error("Application startup failed: %s", e)
131
155
  raise
132
156
 
133
157
 
134
- def _run_mcp_server_sync(api_key: str) -> None:
135
- """Run MCP server in a separate thread."""
158
+ def _start_websocket_connection(api_key: str, main_loop: asyncio.AbstractEventLoop) -> None:
159
+ """Run WebSocket tasks in a separate thread with its own async loop."""
160
+ global websocket_event_loop # noqa: PLW0603
136
161
  try:
137
- from griptape_nodes.mcp_server.server import main_sync
162
+ # Create a new event loop for this thread
163
+ loop = asyncio.new_event_loop()
164
+ websocket_event_loop = loop
165
+ asyncio.set_event_loop(loop)
166
+
167
+ # Signal that websocket_event_loop is ready
168
+ websocket_event_loop_ready.set()
138
169
 
139
- main_sync(api_key)
170
+ # Run the async WebSocket tasks
171
+ loop.run_until_complete(_run_websocket_tasks(api_key, main_loop))
140
172
  except Exception as e:
141
- logger.error("MCP server thread error: %s", e)
173
+ logger.error("WebSocket thread error: %s", e)
142
174
  raise
175
+ finally:
176
+ websocket_event_loop = None
177
+ websocket_event_loop_ready.clear()
143
178
 
144
179
 
145
- def _run_static_server_sync(static_dir: Path) -> None:
146
- """Run static server in a separate thread."""
147
- try:
148
- from .api import start_api
180
+ async def _run_websocket_tasks(api_key: str, main_loop: asyncio.AbstractEventLoop) -> None:
181
+ """Run WebSocket tasks - async version."""
182
+ # Create WebSocket connection for this thread
183
+ connection_stream = _create_websocket_connection(api_key)
149
184
 
150
- start_api(static_dir)
151
- except Exception as e:
152
- logger.error("Static server thread error: %s", e)
153
- raise
185
+ # Track if this is the first connection
186
+ initialized = False
187
+
188
+ async for ws_connection in connection_stream:
189
+ try:
190
+ # Emit initialization event only for the first connection
191
+ if not initialized:
192
+ griptape_nodes.EventManager().put_event_threadsafe(
193
+ main_loop, AppEvent(payload=app_events.AppInitializationComplete())
194
+ )
195
+ initialized = True
196
+
197
+ # Emit connection established event for every connection
198
+ griptape_nodes.EventManager().put_event_threadsafe(
199
+ main_loop, AppEvent(payload=app_events.AppConnectionEstablished())
200
+ )
201
+
202
+ async with asyncio.TaskGroup() as tg:
203
+ tg.create_task(_process_incoming_messages(ws_connection, main_loop))
204
+ tg.create_task(_send_outgoing_messages(ws_connection))
205
+ except* Exception as e:
206
+ logger.error("WebSocket tasks failed: %s", e.exceptions)
207
+ await asyncio.sleep(2.0) # Wait before retry
154
208
 
155
209
 
156
210
  def _ensure_api_key() -> str:
@@ -179,70 +233,156 @@ def _build_static_dir() -> Path:
179
233
  return Path(config_manager.workspace_path) / config_manager.merged_config["static_files_directory"]
180
234
 
181
235
 
182
- async def _listen_for_api_requests(api_key: str) -> None:
183
- """Listen for events and add to async queue."""
184
- logger.info("Listening for events from Nodes API via async WebSocket")
185
-
186
- connection_stream = _create_websocket_connection(api_key)
187
- initialized = False
236
+ async def _process_incoming_messages(ws_connection: Any, main_loop: asyncio.AbstractEventLoop) -> None:
237
+ """Process incoming WebSocket requests from Nodes API."""
238
+ logger.info("Processing incoming WebSocket requests from WebSocket connection")
188
239
 
189
240
  try:
190
- async for ws_connection in connection_stream:
191
- await _handle_websocket_connection(ws_connection, initialized=initialized)
192
- initialized = True
241
+ async for message in ws_connection:
242
+ try:
243
+ data = json.loads(message)
244
+ await _process_api_event(data, main_loop)
245
+ except Exception:
246
+ logger.exception("Error processing event, skipping.")
193
247
 
248
+ except ConnectionClosed:
249
+ logger.info("WebSocket connection closed, will retry")
194
250
  except asyncio.CancelledError:
195
251
  # Clean shutdown when task is cancelled
196
252
  logger.info("WebSocket listener shutdown complete")
197
253
  raise
198
254
  except Exception as e:
199
- logger.error("Fatal error in WebSocket listener: %s", e)
255
+ logger.error("Error in WebSocket connection. Retrying in 2 seconds... %s", e)
256
+ await asyncio.sleep(2.0)
200
257
  raise
201
258
  finally:
202
- await _cleanup_websocket_connection()
259
+ logger.info("WebSocket listener shutdown complete")
203
260
 
204
261
 
205
- async def _handle_websocket_connection(ws_connection: Any, *, initialized: bool) -> None:
206
- """Handle a single WebSocket connection."""
262
+ def _create_websocket_connection(api_key: str) -> Any:
263
+ """Create an async WebSocket connection to the Nodes API."""
264
+ endpoint = urljoin(
265
+ os.getenv("GRIPTAPE_NODES_API_BASE_URL", "https://api.nodes.griptape.ai").replace("http", "ws"),
266
+ "/ws/engines/events?version=v2",
267
+ )
268
+
269
+ return connect(
270
+ endpoint,
271
+ additional_headers={"Authorization": f"Bearer {api_key}"},
272
+ )
273
+
274
+
275
+ async def _process_api_event(event: dict, main_loop: asyncio.AbstractEventLoop) -> None:
276
+ """Process API events and add to async queue."""
277
+ payload = event.get("payload", {})
278
+
207
279
  try:
208
- ws_connection_context.set(ws_connection)
209
- ws_ready_event.set()
280
+ payload["request"]
281
+ except KeyError:
282
+ msg = "Error: 'request' was expected but not found."
283
+ raise RuntimeError(msg) from None
210
284
 
211
- if not initialized:
212
- await griptape_nodes.EventManager().aput_event(AppEvent(payload=app_events.AppInitializationComplete()))
285
+ try:
286
+ event_type = payload["event_type"]
287
+ if event_type != "EventRequest":
288
+ msg = "Error: 'event_type' was found on request, but did not match 'EventRequest' as expected."
289
+ raise RuntimeError(msg) from None
290
+ except KeyError:
291
+ msg = "Error: 'event_type' not found in request."
292
+ raise RuntimeError(msg) from None
213
293
 
214
- await griptape_nodes.EventManager().aput_event(AppEvent(payload=app_events.AppConnectionEstablished()))
294
+ # Now attempt to convert it into an EventRequest.
295
+ try:
296
+ request_event = deserialize_event(json_data=payload)
297
+ except Exception as e:
298
+ msg = f"Unable to convert request JSON into a valid EventRequest object. Error Message: '{e}'"
299
+ raise RuntimeError(msg) from None
300
+
301
+ if not isinstance(request_event, EventRequest):
302
+ msg = f"Deserialized event is not an EventRequest: {type(request_event)}"
303
+ raise TypeError(msg)
304
+
305
+ # Check if the event implements SkipTheLineMixin for priority processing
306
+ if isinstance(request_event.request, SkipTheLineMixin):
307
+ # Handle the event immediately without queuing
308
+ await _process_event_request(request_event)
309
+ else:
310
+ # Add the event to the main thread event queue for processing
311
+ griptape_nodes.EventManager().put_event_threadsafe(main_loop, request_event)
312
+
313
+
314
+ async def _send_outgoing_messages(ws_connection: Any) -> None:
315
+ """Send outgoing WebSocket requests from queue on background thread."""
316
+ logger.info("Starting outgoing WebSocket request sender")
317
+
318
+ try:
319
+ while True:
320
+ # Get message from outgoing queue
321
+ message = await ws_outgoing_queue.get()
215
322
 
216
- async for message in ws_connection:
217
323
  try:
218
- data = json.loads(message)
219
- await _process_api_event(data)
220
- except Exception:
221
- logger.exception("Error processing event, skipping.")
324
+ if isinstance(message, WebSocketMessage):
325
+ await _send_websocket_message(ws_connection, message.event_type, message.payload, message.topic)
326
+ elif isinstance(message, SubscribeCommand):
327
+ await _send_subscribe_command(ws_connection, message.topic)
328
+ elif isinstance(message, UnsubscribeCommand):
329
+ await _send_unsubscribe_command(ws_connection, message.topic)
330
+ else:
331
+ logger.warning("Unknown outgoing message type: %s", type(message))
332
+ except Exception as e:
333
+ logger.error("Error sending outgoing WebSocket request: %s", e)
334
+ finally:
335
+ ws_outgoing_queue.task_done()
222
336
 
223
- except ConnectionClosed:
224
- logger.info("WebSocket connection closed, will retry")
337
+ except asyncio.CancelledError:
338
+ logger.info("Outbound request sender shutdown complete")
339
+ raise
225
340
  except Exception as e:
226
- logger.error("Error in WebSocket connection. Retrying in 2 seconds... %s", e)
227
- await asyncio.sleep(2.0)
228
- finally:
229
- ws_connection_context.set(None)
230
- ws_ready_event.clear()
341
+ logger.error("Fatal error in outgoing request sender: %s", e)
342
+ raise
231
343
 
232
344
 
233
- async def _cleanup_websocket_connection() -> None:
234
- """Clean up WebSocket connection on shutdown."""
235
- ws_connection = ws_connection_context.get()
236
- if ws_connection:
237
- with contextlib.suppress(Exception):
238
- await ws_connection.close()
239
- logger.info("WebSocket listener shutdown complete")
345
+ async def _send_websocket_message(ws_connection: Any, event_type: str, payload: str, topic: str | None) -> None:
346
+ """Send a message via WebSocket."""
347
+ try:
348
+ if topic is None:
349
+ topic = determine_response_topic()
350
+
351
+ body = {"type": event_type, "payload": json.loads(payload), "topic": topic}
352
+ await ws_connection.send(json.dumps(body))
353
+ except WebSocketException as e:
354
+ logger.error("Error sending WebSocket message: %s", e)
355
+ except Exception as e:
356
+ logger.error("Unexpected error sending WebSocket message: %s", e)
357
+
358
+
359
+ async def _send_subscribe_command(ws_connection: Any, topic: str) -> None:
360
+ """Send subscribe command via WebSocket."""
361
+ try:
362
+ body = {"type": "subscribe", "topic": topic, "payload": {}}
363
+ await ws_connection.send(json.dumps(body))
364
+ logger.info("Subscribed to topic: %s", topic)
365
+ except WebSocketException as e:
366
+ logger.error("Error subscribing to topic %s: %s", topic, e)
367
+ except Exception as e:
368
+ logger.error("Unexpected error subscribing to topic %s: %s", topic, e)
369
+
370
+
371
+ async def _send_unsubscribe_command(ws_connection: Any, topic: str) -> None:
372
+ """Send unsubscribe command via WebSocket."""
373
+ try:
374
+ body = {"type": "unsubscribe", "topic": topic, "payload": {}}
375
+ await ws_connection.send(json.dumps(body))
376
+ logger.info("Unsubscribed from topic: %s", topic)
377
+ except WebSocketException as e:
378
+ logger.error("Error unsubscribing from topic %s: %s", topic, e)
379
+ except Exception as e:
380
+ logger.error("Unexpected error unsubscribing from topic %s: %s", topic, e)
240
381
 
241
382
 
242
383
  async def _process_event_queue() -> None:
243
- """Process events concurrently - all events can run simultaneously."""
244
- # Wait for WebSocket connection (convert to async)
245
- await _await_websocket_ready()
384
+ """Process events concurrently - runs on main thread."""
385
+ logger.info("Starting event queue processor on main thread")
246
386
  background_tasks = set()
247
387
 
248
388
  def _handle_task_result(task: asyncio.Task) -> None:
@@ -291,17 +431,7 @@ async def _process_event_request(event: EventRequest) -> None:
291
431
  else:
292
432
  dest_socket = "failure_result"
293
433
 
294
- await __emit_message(dest_socket, result_event.json(), topic=result_event.response_topic)
295
-
296
-
297
- async def _await_websocket_ready() -> None:
298
- """Wait for WebSocket connection to be ready using event coordination."""
299
- websocket_timeout = 15
300
- try:
301
- await asyncio.wait_for(ws_ready_event.wait(), timeout=websocket_timeout)
302
- except TimeoutError:
303
- console.print("[red]WebSocket connection timeout[/red]")
304
- raise
434
+ await _send_message(dest_socket, result_event.json(), topic=result_event.response_topic)
305
435
 
306
436
 
307
437
  async def _process_app_event(event: AppEvent) -> None:
@@ -309,7 +439,7 @@ async def _process_app_event(event: AppEvent) -> None:
309
439
  # Let Griptape Nodes broadcast it.
310
440
  await griptape_nodes.broadcast_app_event(event.payload)
311
441
 
312
- await __emit_message("app_event", event.json())
442
+ await _send_message("app_event", event.json())
313
443
 
314
444
 
315
445
  async def _process_node_event(event: GriptapeNodeEvent) -> None:
@@ -324,12 +454,12 @@ async def _process_node_event(event: GriptapeNodeEvent) -> None:
324
454
  msg = f"Unknown/unsupported result event type encountered: '{type(result_event)}'."
325
455
  raise TypeError(msg) from None
326
456
 
327
- await __emit_message(dest_socket, result_event.json(), topic=result_event.response_topic)
457
+ await _send_message(dest_socket, result_event.json(), topic=result_event.response_topic)
328
458
 
329
459
 
330
460
  async def _process_execution_node_event(event: ExecutionGriptapeNodeEvent) -> None:
331
461
  """Process ExecutionGriptapeNodeEvents and send them to the API (async version)."""
332
- await __emit_message("execution_event", event.wrapped_event.json())
462
+ await _send_message("execution_event", event.wrapped_event.json())
333
463
 
334
464
 
335
465
  async def _process_progress_event(gt_event: ProgressEvent) -> None:
@@ -341,41 +471,47 @@ async def _process_progress_event(gt_event: ProgressEvent) -> None:
341
471
  node_name=node_name, parameter_name=gt_event.parameter_name, type=type(gt_event).__name__, value=value
342
472
  )
343
473
  event_to_emit = ExecutionEvent(payload=payload)
344
- await __emit_message("execution_event", event_to_emit.json())
474
+ await _send_message("execution_event", event_to_emit.json())
345
475
 
346
476
 
347
- def _create_websocket_connection(api_key: str) -> Any:
348
- """Create an async WebSocket connection to the Nodes API."""
349
- endpoint = urljoin(
350
- os.getenv("GRIPTAPE_NODES_API_BASE_URL", "https://api.nodes.griptape.ai").replace("http", "ws"),
351
- "/ws/engines/events?version=v2",
352
- )
477
+ async def _send_message(event_type: str, payload: str, topic: str | None = None) -> None:
478
+ """Queue a message to be sent via WebSocket using run_coroutine_threadsafe."""
479
+ # Wait for websocket event loop to be ready
480
+ websocket_event_loop_ready.wait()
353
481
 
354
- return connect(
355
- endpoint,
356
- additional_headers={"Authorization": f"Bearer {api_key}"},
357
- )
482
+ # Use run_coroutine_threadsafe to put message into WebSocket background thread queue
483
+ if websocket_event_loop is None:
484
+ logger.error("WebSocket event loop not available for message")
485
+ return
486
+
487
+ # Determine topic based on session_id and engine_id in the payload
488
+ if topic is None:
489
+ topic = determine_response_topic()
490
+
491
+ message = WebSocketMessage(event_type, payload, topic)
492
+
493
+ asyncio.run_coroutine_threadsafe(ws_outgoing_queue.put(message), websocket_event_loop)
358
494
 
359
495
 
360
- async def __emit_message(event_type: str, payload: str, topic: str | None = None) -> None:
361
- """Send a message via WebSocket asynchronously."""
362
- ws_connection = ws_connection_context.get()
363
- if ws_connection is None:
364
- logger.warning("WebSocket connection not available for sending message")
496
+ async def subscribe_to_topic(topic: str) -> None:
497
+ """Queue a subscribe command for WebSocket using run_coroutine_threadsafe."""
498
+ # Wait for websocket event loop to be ready
499
+ websocket_event_loop_ready.wait()
500
+
501
+ if websocket_event_loop is None:
502
+ logger.error("WebSocket event loop not available for subscribe")
365
503
  return
366
504
 
367
- try:
368
- # Determine topic based on session_id and engine_id in the payload
369
- if topic is None:
370
- topic = determine_response_topic()
505
+ asyncio.run_coroutine_threadsafe(ws_outgoing_queue.put(SubscribeCommand(topic)), websocket_event_loop)
371
506
 
372
- body = {"type": event_type, "payload": json.loads(payload), "topic": topic}
373
507
 
374
- await ws_connection.send(json.dumps(body))
375
- except WebSocketException as e:
376
- logger.error("Error sending event to Nodes API: %s", e)
377
- except Exception as e:
378
- logger.error("Unexpected error while sending event to Nodes API: %s", e)
508
+ async def unsubscribe_from_topic(topic: str) -> None:
509
+ """Queue an unsubscribe command for WebSocket using run_coroutine_threadsafe."""
510
+ if websocket_event_loop is None:
511
+ logger.error("WebSocket event loop not available for unsubscribe")
512
+ return
513
+
514
+ asyncio.run_coroutine_threadsafe(ws_outgoing_queue.put(UnsubscribeCommand(topic)), websocket_event_loop)
379
515
 
380
516
 
381
517
  def determine_response_topic() -> str | None:
@@ -412,76 +548,3 @@ def determine_request_topic() -> str | None:
412
548
 
413
549
  # Default to generic request topic
414
550
  return "request"
415
-
416
-
417
- async def subscribe_to_topic(topic: str) -> None:
418
- """Subscribe to a specific topic in the message bus."""
419
- ws_connection = ws_connection_context.get()
420
- if ws_connection is None:
421
- logger.warning("WebSocket connection not available for subscribing to topic")
422
- return
423
-
424
- try:
425
- body = {"type": "subscribe", "topic": topic, "payload": {}}
426
- await ws_connection.send(json.dumps(body))
427
- logger.info("Subscribed to topic: %s", topic)
428
- except WebSocketException as e:
429
- logger.error("Error subscribing to topic %s: %s", topic, e)
430
- except Exception as e:
431
- logger.error("Unexpected error while subscribing to topic %s: %s", topic, e)
432
-
433
-
434
- async def unsubscribe_from_topic(topic: str) -> None:
435
- """Unsubscribe from a specific topic in the message bus."""
436
- ws_connection = ws_connection_context.get()
437
- if ws_connection is None:
438
- logger.warning("WebSocket connection not available for unsubscribing from topic")
439
- return
440
-
441
- try:
442
- body = {"type": "unsubscribe", "topic": topic, "payload": {}}
443
- await ws_connection.send(json.dumps(body))
444
- logger.info("Unsubscribed from topic: %s", topic)
445
- except WebSocketException as e:
446
- logger.error("Error unsubscribing from topic %s: %s", topic, e)
447
- except Exception as e:
448
- logger.error("Unexpected error while unsubscribing from topic %s: %s", topic, e)
449
-
450
-
451
- async def _process_api_event(event: dict) -> None:
452
- """Process API events and add to async queue."""
453
- payload = event.get("payload", {})
454
-
455
- try:
456
- payload["request"]
457
- except KeyError:
458
- msg = "Error: 'request' was expected but not found."
459
- raise RuntimeError(msg) from None
460
-
461
- try:
462
- event_type = payload["event_type"]
463
- if event_type != "EventRequest":
464
- msg = "Error: 'event_type' was found on request, but did not match 'EventRequest' as expected."
465
- raise RuntimeError(msg) from None
466
- except KeyError:
467
- msg = "Error: 'event_type' not found in request."
468
- raise RuntimeError(msg) from None
469
-
470
- # Now attempt to convert it into an EventRequest.
471
- try:
472
- request_event = deserialize_event(json_data=payload)
473
- except Exception as e:
474
- msg = f"Unable to convert request JSON into a valid EventRequest object. Error Message: '{e}'"
475
- raise RuntimeError(msg) from None
476
-
477
- if not isinstance(request_event, EventRequest):
478
- msg = f"Deserialized event is not an EventRequest: {type(request_event)}"
479
- raise TypeError(msg)
480
-
481
- # Check if the event implements SkipTheLineMixin for priority processing
482
- if isinstance(request_event.request, SkipTheLineMixin):
483
- # Handle the event immediately without queuing
484
- await _process_event_request(request_event)
485
- else:
486
- # Add the event to the async queue for normal processing
487
- await griptape_nodes.EventManager().aput_event(request_event)
@@ -69,7 +69,7 @@ mcp_server_logger.addHandler(RichHandler(show_time=True, show_path=False, markup
69
69
  mcp_server_logger.setLevel(logging.INFO)
70
70
 
71
71
 
72
- def main_sync(api_key: str) -> None:
72
+ def start_mcp_server(api_key: str) -> None:
73
73
  """Synchronous version of main entry point for the Griptape Nodes MCP server."""
74
74
  mcp_server_logger.debug("Starting MCP GTN server...")
75
75
  # Give these a session ID
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: griptape-nodes
3
- Version: 0.52.0
3
+ Version: 0.52.1
4
4
  Summary: Add your description here
5
5
  Requires-Dist: griptape>=1.8.2
6
6
  Requires-Dist: pydantic>=2.10.6
@@ -1,8 +1,8 @@
1
1
  griptape_nodes/__init__.py,sha256=gKRELLnKTgmejKncdssrWFz7k0Aj_Fx7TatcEJB9H-A,37424
2
2
  griptape_nodes/app/.python-version,sha256=e1X45ntWI8S-8_ppEojalDfXnTq6FW3kjUgdsyrH0W0,5
3
3
  griptape_nodes/app/__init__.py,sha256=DB-DTsgcNnbmEClXEouwzGhrmo3gHBCWXB9BkPGpdQI,90
4
- griptape_nodes/app/api.py,sha256=w-6FRXRo5yrgnYYG8ClUKVLO0nhKso9nSOJD-P9iRLw,6858
5
- griptape_nodes/app/app.py,sha256=8r06kohQrY2vv0xhcTg1tig3ZUfUZ4HtUwfML3rYNus,17958
4
+ griptape_nodes/app/api.py,sha256=5IQgyXtcJeppwBpGmLdAQCOc5kh0KX2t1WOqkpo8P30,6579
5
+ griptape_nodes/app/app.py,sha256=OoRVe4QR5DTOVPywZF_ffLsGWt7cBR33VviFGSY_cmA,20727
6
6
  griptape_nodes/app/watch.py,sha256=WE3P0Hl_jDezqy_UetNK2K5NTQFYKcoEbcKwzRWY4MU,2648
7
7
  griptape_nodes/bootstrap/__init__.py,sha256=ENv3SIzQ9TtlRrg1y4e4CnoBpJaFpFSkNpTFBV8X5Ls,25
8
8
  griptape_nodes/bootstrap/workflow_executors/__init__.py,sha256=pyjN81-eLtjyECFYLXOtMCixiiI9qBi5yald86iM7Ek,34
@@ -25,7 +25,7 @@ griptape_nodes/machines/control_flow.py,sha256=x-GlCjfMp-B1BvElcIOxOHtXADx1eK1JN
25
25
  griptape_nodes/machines/fsm.py,sha256=JXf4VgaLMEcjDuCxuKyWlAmK5PCOrWHsMau6OPjhL3s,2344
26
26
  griptape_nodes/machines/node_resolution.py,sha256=ruvSI5_q6S1O5kNypavxT-feduQpVEvRKrF5ZTPJdL8,18458
27
27
  griptape_nodes/mcp_server/__init__.py,sha256=GSpJWqE4lICaryhsQR1okeMH2x6j1bBL0HVxtr52WLg,42
28
- griptape_nodes/mcp_server/server.py,sha256=7UvgUIATvG9gVD17bHwfWzerm6L5ME8qY319tnlkzac,5210
28
+ griptape_nodes/mcp_server/server.py,sha256=X1ulGyHqQCFiRnJ8PgJh4lf83k62a_D9vFQThgKUaMw,5217
29
29
  griptape_nodes/mcp_server/ws_request_manager.py,sha256=UpXOKNq_VauQmJ0Rx0C7OrzcwjeCoDfjpx3IE38NgsE,10949
30
30
  griptape_nodes/node_library/__init__.py,sha256=U3FcSdmq6UW7qt6E3Up3NWKvUEn5_5lqL-u5qbzfxMQ,28
31
31
  griptape_nodes/node_library/advanced_node_library.py,sha256=B1ZaxuFIzQ6tx_3MLIxlsHuahthEC1Hw_t6K_ByIdzs,2104
@@ -124,7 +124,7 @@ griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_re
124
124
  griptape_nodes/version_compatibility/workflow_versions/__init__.py,sha256=z5XDgkizoNByCXpyo34hfsJKFsWlOHbD6hgzfYH9ubc,52
125
125
  griptape_nodes/version_compatibility/workflow_versions/v0_7_0/__init__.py,sha256=IzPPmGK86h2swfGGTOHyVcBIlOng6SjgWQzlbf3ngmo,51
126
126
  griptape_nodes/version_compatibility/workflow_versions/v0_7_0/local_executor_argument_addition.py,sha256=9PclAp_Mm5IPtd5yj5XSS5-x7QYmifvhTly20CgBZmo,2018
127
- griptape_nodes-0.52.0.dist-info/WHEEL,sha256=Jb20R3Ili4n9P1fcwuLup21eQ5r9WXhs4_qy7VTrgPI,79
128
- griptape_nodes-0.52.0.dist-info/entry_points.txt,sha256=qvevqd3BVbAV5TcantnAm0ouqaqYKhsRO3pkFymWLWM,82
129
- griptape_nodes-0.52.0.dist-info/METADATA,sha256=Syb9Euum5ggkxmZA554heaE0qb4Qc6MAd1cXHWMevx4,4943
130
- griptape_nodes-0.52.0.dist-info/RECORD,,
127
+ griptape_nodes-0.52.1.dist-info/WHEEL,sha256=Jb20R3Ili4n9P1fcwuLup21eQ5r9WXhs4_qy7VTrgPI,79
128
+ griptape_nodes-0.52.1.dist-info/entry_points.txt,sha256=qvevqd3BVbAV5TcantnAm0ouqaqYKhsRO3pkFymWLWM,82
129
+ griptape_nodes-0.52.1.dist-info/METADATA,sha256=SvH75XQLx75G84IO6Sgb2Q8yq8PRzFKPeJ3dt5uaB3A,4943
130
+ griptape_nodes-0.52.1.dist-info/RECORD,,