griptape-nodes 0.42.0__py3-none-any.whl → 0.43.0__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/__init__.py +0 -0
- griptape_nodes/app/.python-version +0 -0
- griptape_nodes/app/__init__.py +1 -6
- griptape_nodes/app/api.py +199 -0
- griptape_nodes/app/app.py +140 -225
- griptape_nodes/app/watch.py +1 -1
- griptape_nodes/bootstrap/__init__.py +0 -0
- griptape_nodes/bootstrap/bootstrap_script.py +0 -0
- griptape_nodes/bootstrap/register_libraries_script.py +0 -0
- griptape_nodes/bootstrap/structure_config.yaml +0 -0
- griptape_nodes/bootstrap/workflow_executors/__init__.py +0 -0
- griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +0 -0
- griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +0 -0
- griptape_nodes/bootstrap/workflow_runners/__init__.py +0 -0
- griptape_nodes/bootstrap/workflow_runners/bootstrap_workflow_runner.py +0 -0
- griptape_nodes/bootstrap/workflow_runners/local_workflow_runner.py +0 -0
- griptape_nodes/bootstrap/workflow_runners/subprocess_workflow_runner.py +6 -2
- griptape_nodes/bootstrap/workflow_runners/workflow_runner.py +0 -0
- griptape_nodes/drivers/__init__.py +0 -0
- griptape_nodes/drivers/storage/__init__.py +0 -0
- griptape_nodes/drivers/storage/base_storage_driver.py +0 -0
- griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +0 -0
- griptape_nodes/drivers/storage/local_storage_driver.py +2 -1
- griptape_nodes/drivers/storage/storage_backend.py +0 -0
- griptape_nodes/exe_types/__init__.py +0 -0
- griptape_nodes/exe_types/connections.py +0 -0
- griptape_nodes/exe_types/core_types.py +0 -0
- griptape_nodes/exe_types/flow.py +0 -0
- griptape_nodes/exe_types/node_types.py +17 -1
- griptape_nodes/exe_types/type_validator.py +0 -0
- griptape_nodes/machines/__init__.py +0 -0
- griptape_nodes/machines/control_flow.py +41 -12
- griptape_nodes/machines/fsm.py +16 -2
- griptape_nodes/machines/node_resolution.py +0 -0
- griptape_nodes/mcp_server/__init__.py +1 -0
- griptape_nodes/mcp_server/server.py +126 -0
- griptape_nodes/mcp_server/ws_request_manager.py +268 -0
- griptape_nodes/node_library/__init__.py +0 -0
- griptape_nodes/node_library/advanced_node_library.py +0 -0
- griptape_nodes/node_library/library_registry.py +0 -0
- griptape_nodes/node_library/workflow_registry.py +1 -1
- griptape_nodes/py.typed +0 -0
- griptape_nodes/retained_mode/__init__.py +0 -0
- griptape_nodes/retained_mode/events/__init__.py +0 -0
- griptape_nodes/retained_mode/events/agent_events.py +0 -0
- griptape_nodes/retained_mode/events/app_events.py +6 -2
- griptape_nodes/retained_mode/events/arbitrary_python_events.py +0 -0
- griptape_nodes/retained_mode/events/base_events.py +6 -6
- griptape_nodes/retained_mode/events/config_events.py +0 -0
- griptape_nodes/retained_mode/events/connection_events.py +0 -0
- griptape_nodes/retained_mode/events/context_events.py +0 -0
- griptape_nodes/retained_mode/events/execution_events.py +0 -0
- griptape_nodes/retained_mode/events/flow_events.py +0 -0
- griptape_nodes/retained_mode/events/generate_request_payload_schemas.py +0 -0
- griptape_nodes/retained_mode/events/library_events.py +2 -2
- griptape_nodes/retained_mode/events/logger_events.py +0 -0
- griptape_nodes/retained_mode/events/node_events.py +0 -0
- griptape_nodes/retained_mode/events/object_events.py +0 -0
- griptape_nodes/retained_mode/events/os_events.py +104 -2
- griptape_nodes/retained_mode/events/parameter_events.py +0 -0
- griptape_nodes/retained_mode/events/payload_registry.py +0 -0
- griptape_nodes/retained_mode/events/secrets_events.py +0 -0
- griptape_nodes/retained_mode/events/static_file_events.py +0 -0
- griptape_nodes/retained_mode/events/validation_events.py +0 -0
- griptape_nodes/retained_mode/events/workflow_events.py +0 -0
- griptape_nodes/retained_mode/griptape_nodes.py +43 -40
- griptape_nodes/retained_mode/managers/__init__.py +0 -0
- griptape_nodes/retained_mode/managers/agent_manager.py +48 -22
- griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +0 -0
- griptape_nodes/retained_mode/managers/config_manager.py +0 -0
- griptape_nodes/retained_mode/managers/context_manager.py +0 -0
- griptape_nodes/retained_mode/managers/engine_identity_manager.py +0 -0
- griptape_nodes/retained_mode/managers/event_manager.py +0 -0
- griptape_nodes/retained_mode/managers/flow_manager.py +2 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/__init__.py +45 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/data_models.py +191 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +346 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_fsm.py +439 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/__init__.py +17 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/base.py +82 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/github.py +116 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +352 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/package.py +104 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/sandbox.py +155 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance.py +18 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_status.py +12 -0
- griptape_nodes/retained_mode/managers/library_manager.py +144 -39
- griptape_nodes/retained_mode/managers/node_manager.py +86 -72
- griptape_nodes/retained_mode/managers/object_manager.py +0 -0
- griptape_nodes/retained_mode/managers/operation_manager.py +0 -0
- griptape_nodes/retained_mode/managers/os_manager.py +517 -12
- griptape_nodes/retained_mode/managers/secrets_manager.py +0 -0
- griptape_nodes/retained_mode/managers/session_manager.py +0 -0
- griptape_nodes/retained_mode/managers/settings.py +0 -0
- griptape_nodes/retained_mode/managers/static_files_manager.py +0 -0
- griptape_nodes/retained_mode/managers/version_compatibility_manager.py +2 -2
- griptape_nodes/retained_mode/managers/workflow_manager.py +199 -2
- griptape_nodes/retained_mode/retained_mode.py +0 -0
- griptape_nodes/retained_mode/utils/__init__.py +0 -0
- griptape_nodes/retained_mode/utils/engine_identity.py +0 -0
- griptape_nodes/retained_mode/utils/name_generator.py +0 -0
- griptape_nodes/traits/__init__.py +0 -0
- griptape_nodes/traits/add_param_button.py +0 -0
- griptape_nodes/traits/button.py +0 -0
- griptape_nodes/traits/clamp.py +0 -0
- griptape_nodes/traits/compare.py +0 -0
- griptape_nodes/traits/compare_images.py +0 -0
- griptape_nodes/traits/file_system_picker.py +127 -0
- griptape_nodes/traits/minmax.py +0 -0
- griptape_nodes/traits/options.py +0 -0
- griptape_nodes/traits/slider.py +0 -0
- griptape_nodes/traits/trait_registry.py +0 -0
- griptape_nodes/traits/traits.json +0 -0
- griptape_nodes/updater/__init__.py +2 -2
- griptape_nodes/updater/__main__.py +0 -0
- griptape_nodes/utils/__init__.py +0 -0
- griptape_nodes/utils/dict_utils.py +0 -0
- griptape_nodes/utils/image_preview.py +128 -0
- griptape_nodes/utils/metaclasses.py +0 -0
- griptape_nodes/version_compatibility/__init__.py +0 -0
- griptape_nodes/version_compatibility/versions/__init__.py +0 -0
- griptape_nodes/version_compatibility/versions/v0_39_0/__init__.py +0 -0
- griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +5 -5
- griptape_nodes-0.43.0.dist-info/METADATA +90 -0
- griptape_nodes-0.43.0.dist-info/RECORD +129 -0
- griptape_nodes-0.43.0.dist-info/WHEEL +4 -0
- {griptape_nodes-0.42.0.dist-info → griptape_nodes-0.43.0.dist-info}/entry_points.txt +1 -0
- griptape_nodes/app/app_sessions.py +0 -554
- griptape_nodes-0.42.0.dist-info/METADATA +0 -78
- griptape_nodes-0.42.0.dist-info/RECORD +0 -113
- griptape_nodes-0.42.0.dist-info/WHEEL +0 -4
- griptape_nodes-0.42.0.dist-info/licenses/LICENSE +0 -201
|
@@ -65,6 +65,9 @@ class BaseNode(ABC):
|
|
|
65
65
|
stop_flow: bool = False
|
|
66
66
|
root_ui_element: BaseNodeElement
|
|
67
67
|
_tracked_parameters: list[BaseNodeElement]
|
|
68
|
+
_entry_control_parameter: Parameter | None = (
|
|
69
|
+
None # The control input parameter used to enter this node during execution
|
|
70
|
+
)
|
|
68
71
|
|
|
69
72
|
@property
|
|
70
73
|
def parameters(self) -> list[Parameter]:
|
|
@@ -92,6 +95,7 @@ class BaseNode(ABC):
|
|
|
92
95
|
self.root_ui_element._node_context = self
|
|
93
96
|
self.process_generator = None
|
|
94
97
|
self._tracked_parameters = []
|
|
98
|
+
self.set_entry_control_parameter(None)
|
|
95
99
|
|
|
96
100
|
# This is gross and we need to have a universal pass on resolution state changes and emission of events. That's what this ticket does!
|
|
97
101
|
# https://github.com/griptape-ai/griptape-nodes/issues/994
|
|
@@ -106,6 +110,18 @@ class BaseNode(ABC):
|
|
|
106
110
|
)
|
|
107
111
|
)
|
|
108
112
|
self.state = NodeResolutionState.UNRESOLVED
|
|
113
|
+
# NOTE: _entry_control_parameter is NOT cleared here as it represents execution context
|
|
114
|
+
# that should persist through the resolve/unresolve cycle during a single execution
|
|
115
|
+
|
|
116
|
+
def set_entry_control_parameter(self, parameter: Parameter | None) -> None:
|
|
117
|
+
"""Set the control parameter that was used to enter this node.
|
|
118
|
+
|
|
119
|
+
This should only be called by the ControlFlowContext during execution.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
parameter: The control input parameter that triggered this node's execution, or None to clear
|
|
123
|
+
"""
|
|
124
|
+
self._entry_control_parameter = parameter
|
|
109
125
|
|
|
110
126
|
def emit_parameter_changes(self) -> None:
|
|
111
127
|
if self._tracked_parameters:
|
|
@@ -577,7 +593,7 @@ class BaseNode(ABC):
|
|
|
577
593
|
|
|
578
594
|
def _flatten(items: Iterable[Any]) -> Generator[Any, None, None]:
|
|
579
595
|
for item in items:
|
|
580
|
-
if isinstance(item, Iterable) and not isinstance(item, (str, bytes)):
|
|
596
|
+
if isinstance(item, Iterable) and not isinstance(item, (str, bytes, dict)):
|
|
581
597
|
yield from _flatten(item)
|
|
582
598
|
elif item:
|
|
583
599
|
yield item
|
|
File without changes
|
|
File without changes
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import logging
|
|
5
|
+
from dataclasses import dataclass
|
|
5
6
|
from typing import TYPE_CHECKING
|
|
6
7
|
|
|
7
8
|
from griptape.events import EventBus
|
|
8
9
|
|
|
10
|
+
from griptape_nodes.exe_types.core_types import Parameter
|
|
9
11
|
from griptape_nodes.exe_types.node_types import BaseNode, NodeResolutionState
|
|
10
12
|
from griptape_nodes.exe_types.type_validator import TypeValidator
|
|
11
13
|
from griptape_nodes.machines.fsm import FSM, State
|
|
@@ -17,6 +19,15 @@ from griptape_nodes.retained_mode.events.execution_events import (
|
|
|
17
19
|
SelectedControlOutputEvent,
|
|
18
20
|
)
|
|
19
21
|
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class NextNodeInfo:
|
|
25
|
+
"""Information about the next node to execute and how to reach it."""
|
|
26
|
+
|
|
27
|
+
node: BaseNode
|
|
28
|
+
entry_parameter: Parameter | None
|
|
29
|
+
|
|
30
|
+
|
|
20
31
|
if TYPE_CHECKING:
|
|
21
32
|
from griptape_nodes.exe_types.core_types import Parameter
|
|
22
33
|
from griptape_nodes.exe_types.flow import ControlFlow
|
|
@@ -36,18 +47,26 @@ class ControlFlowContext:
|
|
|
36
47
|
self.resolution_machine = NodeResolutionMachine()
|
|
37
48
|
self.current_node = None
|
|
38
49
|
|
|
39
|
-
def get_next_node(self, output_parameter: Parameter) ->
|
|
50
|
+
def get_next_node(self, output_parameter: Parameter) -> NextNodeInfo | None:
|
|
51
|
+
"""Get the next node and the target parameter that will receive the control flow.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
NextNodeInfo | None: Information about the next node or None if no connection
|
|
55
|
+
"""
|
|
40
56
|
if self.current_node is not None:
|
|
41
57
|
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
42
58
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
59
|
+
node_connection = (
|
|
60
|
+
GriptapeNodes.FlowManager().get_connections().get_connected_node(self.current_node, output_parameter)
|
|
61
|
+
)
|
|
62
|
+
if node_connection is not None:
|
|
63
|
+
node, entry_parameter = node_connection
|
|
64
|
+
return NextNodeInfo(node=node, entry_parameter=entry_parameter)
|
|
46
65
|
# Continue Execution to the next node that needs to be executed using global execution queue
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
66
|
+
# Get the next node in the execution queue, or None if queue is empty
|
|
67
|
+
node = GriptapeNodes.FlowManager().get_next_node_from_execution_queue()
|
|
68
|
+
if node is not None:
|
|
69
|
+
return NextNodeInfo(node=node, entry_parameter=None)
|
|
51
70
|
return None
|
|
52
71
|
|
|
53
72
|
def reset(self) -> None:
|
|
@@ -111,10 +130,11 @@ class NextNodeState(State):
|
|
|
111
130
|
context.current_node.stop_flow = False
|
|
112
131
|
return CompleteState
|
|
113
132
|
next_output = context.current_node.get_next_control_output()
|
|
114
|
-
|
|
133
|
+
next_node_info = None
|
|
134
|
+
|
|
115
135
|
if next_output is not None:
|
|
116
136
|
context.selected_output = next_output
|
|
117
|
-
|
|
137
|
+
next_node_info = context.get_next_node(context.selected_output)
|
|
118
138
|
EventBus.publish_event(
|
|
119
139
|
ExecutionGriptapeNodeEvent(
|
|
120
140
|
wrapped_event=ExecutionEvent(
|
|
@@ -130,11 +150,18 @@ class NextNodeState(State):
|
|
|
130
150
|
|
|
131
151
|
# Get the next node in the execution queue, or None if queue is empty
|
|
132
152
|
next_node = GriptapeNodes.FlowManager().get_next_node_from_execution_queue()
|
|
153
|
+
if next_node is not None:
|
|
154
|
+
next_node_info = NextNodeInfo(node=next_node, entry_parameter=None)
|
|
155
|
+
|
|
133
156
|
# The parameter that will be evaluated next
|
|
134
|
-
if
|
|
157
|
+
if next_node_info is None:
|
|
135
158
|
# If no node attached
|
|
136
159
|
return CompleteState
|
|
137
|
-
|
|
160
|
+
|
|
161
|
+
# Always set the entry control parameter (None for execution queue nodes)
|
|
162
|
+
next_node_info.node.set_entry_control_parameter(next_node_info.entry_parameter)
|
|
163
|
+
|
|
164
|
+
context.current_node = next_node_info.node
|
|
138
165
|
context.selected_output = None
|
|
139
166
|
if not context.paused:
|
|
140
167
|
return ResolveNodeState
|
|
@@ -177,6 +204,8 @@ class ControlFlowMachine(FSM[ControlFlowContext]):
|
|
|
177
204
|
|
|
178
205
|
def start_flow(self, start_node: BaseNode, debug_mode: bool = False) -> None: # noqa: FBT001, FBT002
|
|
179
206
|
self._context.current_node = start_node
|
|
207
|
+
# Set entry control parameter for initial node (None for workflow start)
|
|
208
|
+
start_node.set_entry_control_parameter(None)
|
|
180
209
|
# Set up to debug
|
|
181
210
|
self._context.paused = debug_mode
|
|
182
211
|
self.start(ResolveNodeState) # Begins the flow
|
griptape_nodes/machines/fsm.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any,
|
|
1
|
+
from typing import Any, TypeVar
|
|
2
2
|
|
|
3
3
|
T = TypeVar("T")
|
|
4
4
|
|
|
@@ -19,8 +19,13 @@ class State:
|
|
|
19
19
|
"""Called when exiting the state."""
|
|
20
20
|
return
|
|
21
21
|
|
|
22
|
+
@staticmethod
|
|
23
|
+
def on_event(context: Any, event: Any) -> type["State"] | None: # noqa: ARG004
|
|
24
|
+
"""Called on an event, which may trigger a State transition."""
|
|
25
|
+
return None
|
|
26
|
+
|
|
22
27
|
|
|
23
|
-
class FSM
|
|
28
|
+
class FSM[T]:
|
|
24
29
|
def __init__(self, context: T) -> None:
|
|
25
30
|
self._context = context
|
|
26
31
|
self._current_state = None
|
|
@@ -53,3 +58,12 @@ class FSM(Generic[T]):
|
|
|
53
58
|
|
|
54
59
|
if new_state is not None:
|
|
55
60
|
self.transition_state(new_state)
|
|
61
|
+
|
|
62
|
+
def handle_event(self, event: Any) -> None:
|
|
63
|
+
if self._current_state is None:
|
|
64
|
+
new_state = None
|
|
65
|
+
else:
|
|
66
|
+
new_state = self._current_state.on_event(self._context, event)
|
|
67
|
+
|
|
68
|
+
if new_state is not None:
|
|
69
|
+
self.transition_state(new_state)
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Runs the Griptape Nodes MCP server."""
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from collections.abc import AsyncIterator
|
|
6
|
+
|
|
7
|
+
import uvicorn
|
|
8
|
+
from fastapi import FastAPI
|
|
9
|
+
from mcp.server.lowlevel import Server
|
|
10
|
+
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
|
11
|
+
from mcp.types import (
|
|
12
|
+
TextContent,
|
|
13
|
+
Tool,
|
|
14
|
+
)
|
|
15
|
+
from pydantic import TypeAdapter
|
|
16
|
+
from rich.logging import RichHandler
|
|
17
|
+
from starlette.types import Receive, Scope, Send
|
|
18
|
+
|
|
19
|
+
from griptape_nodes.mcp_server.ws_request_manager import AsyncRequestManager, WebSocketConnectionManager
|
|
20
|
+
from griptape_nodes.retained_mode.events.base_events import RequestPayload
|
|
21
|
+
from griptape_nodes.retained_mode.events.connection_events import (
|
|
22
|
+
CreateConnectionRequest,
|
|
23
|
+
DeleteConnectionRequest,
|
|
24
|
+
ListConnectionsForNodeRequest,
|
|
25
|
+
)
|
|
26
|
+
from griptape_nodes.retained_mode.events.flow_events import ListNodesInFlowRequest
|
|
27
|
+
from griptape_nodes.retained_mode.events.node_events import (
|
|
28
|
+
CreateNodeRequest,
|
|
29
|
+
DeleteNodeRequest,
|
|
30
|
+
GetNodeMetadataRequest,
|
|
31
|
+
GetNodeResolutionStateRequest,
|
|
32
|
+
ListParametersOnNodeRequest,
|
|
33
|
+
SetNodeMetadataRequest,
|
|
34
|
+
)
|
|
35
|
+
from griptape_nodes.retained_mode.events.parameter_events import (
|
|
36
|
+
GetParameterValueRequest,
|
|
37
|
+
SetParameterValueRequest,
|
|
38
|
+
)
|
|
39
|
+
from griptape_nodes.retained_mode.managers.config_manager import ConfigManager
|
|
40
|
+
from griptape_nodes.retained_mode.managers.secrets_manager import SecretsManager
|
|
41
|
+
|
|
42
|
+
SUPPORTED_REQUEST_EVENTS: dict[str, type[RequestPayload]] = {
|
|
43
|
+
# Nodes
|
|
44
|
+
"CreateNodeRequest": CreateNodeRequest,
|
|
45
|
+
"DeleteNodeRequest": DeleteNodeRequest,
|
|
46
|
+
"ListNodesInFlowRequest": ListNodesInFlowRequest,
|
|
47
|
+
"GetNodeResolutionStateRequest": GetNodeResolutionStateRequest,
|
|
48
|
+
"GetNodeMetadataRequest": GetNodeMetadataRequest,
|
|
49
|
+
"SetNodeMetadataRequest": SetNodeMetadataRequest,
|
|
50
|
+
# Connections
|
|
51
|
+
"CreateConnectionRequest": CreateConnectionRequest,
|
|
52
|
+
"DeleteConnectionRequest": DeleteConnectionRequest,
|
|
53
|
+
"ListConnectionsForNodeRequest": ListConnectionsForNodeRequest,
|
|
54
|
+
# Parameters
|
|
55
|
+
"ListParametersOnNodeRequest": ListParametersOnNodeRequest,
|
|
56
|
+
"GetParameterValueRequest": GetParameterValueRequest,
|
|
57
|
+
"SetParameterValueRequest": SetParameterValueRequest,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
GTN_MCP_SERVER_PORT = int(os.getenv("GTN_MCP_SERVER_PORT", "9927"))
|
|
61
|
+
|
|
62
|
+
config_manager = ConfigManager()
|
|
63
|
+
secrets_manager = SecretsManager(config_manager)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main(api_key: str) -> None:
|
|
67
|
+
"""Main entry point for the Griptape Nodes MCP server."""
|
|
68
|
+
mcp_server_logger = logging.getLogger("griptape_nodes_mcp_server")
|
|
69
|
+
mcp_server_logger.addHandler(RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True))
|
|
70
|
+
mcp_server_logger.setLevel(logging.INFO)
|
|
71
|
+
mcp_server_logger.info("Starting MCP GTN server...")
|
|
72
|
+
|
|
73
|
+
# Give these a session ID
|
|
74
|
+
connection_manager = WebSocketConnectionManager()
|
|
75
|
+
request_manager = AsyncRequestManager(connection_manager, api_key)
|
|
76
|
+
|
|
77
|
+
app = Server("mcp-gtn")
|
|
78
|
+
|
|
79
|
+
@app.list_tools()
|
|
80
|
+
async def list_tools() -> list[Tool]:
|
|
81
|
+
return [
|
|
82
|
+
Tool(name=event.__name__, description=event.__doc__, inputSchema=TypeAdapter(event).json_schema())
|
|
83
|
+
for (name, event) in SUPPORTED_REQUEST_EVENTS.items()
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
@app.call_tool()
|
|
87
|
+
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|
88
|
+
if name not in SUPPORTED_REQUEST_EVENTS:
|
|
89
|
+
msg = f"Unsupported tool: {name}"
|
|
90
|
+
raise ValueError(msg)
|
|
91
|
+
|
|
92
|
+
request_payload = SUPPORTED_REQUEST_EVENTS[name](**arguments)
|
|
93
|
+
|
|
94
|
+
await request_manager.connect()
|
|
95
|
+
result = await request_manager.create_request_event(
|
|
96
|
+
request_payload.__class__.__name__, request_payload.__dict__, timeout_ms=5000
|
|
97
|
+
)
|
|
98
|
+
mcp_server_logger.debug("Got result: %s", result)
|
|
99
|
+
|
|
100
|
+
return [TextContent(type="text", text=json.dumps(result))]
|
|
101
|
+
|
|
102
|
+
# Create the session manager with our app and event store
|
|
103
|
+
session_manager = StreamableHTTPSessionManager(
|
|
104
|
+
app=app,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
@contextlib.asynccontextmanager
|
|
108
|
+
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
|
|
109
|
+
"""Context manager for managing session manager lifecycle."""
|
|
110
|
+
async with session_manager.run():
|
|
111
|
+
mcp_server_logger.info("GTN MCP server started with StreamableHTTP session manager!")
|
|
112
|
+
try:
|
|
113
|
+
yield
|
|
114
|
+
finally:
|
|
115
|
+
mcp_server_logger.info("GTN MCP server shutting down...")
|
|
116
|
+
|
|
117
|
+
# Create an ASGI application using the transport
|
|
118
|
+
mcp_server_app = FastAPI(lifespan=lifespan)
|
|
119
|
+
|
|
120
|
+
# ASGI handler for streamable HTTP connections
|
|
121
|
+
async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> None:
|
|
122
|
+
await session_manager.handle_request(scope, receive, send)
|
|
123
|
+
|
|
124
|
+
mcp_server_app.mount("/mcp", app=handle_streamable_http)
|
|
125
|
+
|
|
126
|
+
uvicorn.run(mcp_server_app, host="127.0.0.1", port=GTN_MCP_SERVER_PORT)
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import uuid
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import Any, Generic, TypeVar
|
|
9
|
+
from urllib.parse import urljoin
|
|
10
|
+
|
|
11
|
+
import websockets
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("griptape_nodes_mcp_server")
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WebSocketConnectionManager:
|
|
19
|
+
"""Python equivalent of WebSocketConnectionManager in TypeScript."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
websocket_url: str = urljoin(
|
|
24
|
+
os.getenv("GRIPTAPE_NODES_API_BASE_URL", "https://api.nodes.griptape.ai").replace("http", "ws"),
|
|
25
|
+
"/ws/engines/events?version=v2",
|
|
26
|
+
),
|
|
27
|
+
):
|
|
28
|
+
self.websocket_url = websocket_url
|
|
29
|
+
self.websocket: Any = None
|
|
30
|
+
self.connected = False
|
|
31
|
+
self.event_handlers: dict[str, list[Callable]] = {}
|
|
32
|
+
self.request_handlers: dict[str, tuple[Callable, Callable]] = {}
|
|
33
|
+
self._process_task: asyncio.Task | None = None
|
|
34
|
+
|
|
35
|
+
async def send(self, data: dict[str, Any]) -> None:
|
|
36
|
+
"""Send a message to the WebSocket server."""
|
|
37
|
+
if not self.websocket:
|
|
38
|
+
msg = "Not connected to WebSocket server"
|
|
39
|
+
raise ConnectionError(msg)
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
message = json.dumps(data)
|
|
43
|
+
await self.websocket.send(message)
|
|
44
|
+
logger.debug("📤 Sent message: %s", message)
|
|
45
|
+
except Exception as e:
|
|
46
|
+
logger.error("Failed to send message: %s", e)
|
|
47
|
+
raise
|
|
48
|
+
|
|
49
|
+
async def _process_messages(self) -> None:
|
|
50
|
+
"""Process incoming WebSocket messages."""
|
|
51
|
+
if not self.websocket:
|
|
52
|
+
logger.warning("WebSocket is not connected, cannot process messages")
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
async for message in self.websocket:
|
|
57
|
+
try:
|
|
58
|
+
data = json.loads(message)
|
|
59
|
+
logger.debug("📥 Received message: %s", message)
|
|
60
|
+
await self._handle_message(data)
|
|
61
|
+
except json.JSONDecodeError:
|
|
62
|
+
logger.error("Failed to parse message: %s", message)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logger.error("Error processing message: %s", e)
|
|
65
|
+
except websockets.exceptions.ConnectionClosed:
|
|
66
|
+
logger.warning("WebSocket connection closed")
|
|
67
|
+
self.connected = False
|
|
68
|
+
except asyncio.CancelledError:
|
|
69
|
+
# Task was cancelled, just exit
|
|
70
|
+
pass
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logger.error("Error in message processing loop: %s", e)
|
|
73
|
+
self.connected = False
|
|
74
|
+
|
|
75
|
+
async def _handle_message(self, data: dict[str, Any]) -> None:
|
|
76
|
+
request = data.get("payload", {}).get("request", {})
|
|
77
|
+
request_id = request.get("request_id")
|
|
78
|
+
|
|
79
|
+
if request_id and request_id in self.request_handlers:
|
|
80
|
+
success_handler, failure_handler = self.request_handlers[request_id]
|
|
81
|
+
try:
|
|
82
|
+
if data.get("type") == "success_result":
|
|
83
|
+
success_handler(data, request)
|
|
84
|
+
else:
|
|
85
|
+
failure_handler(data, request)
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.error("Error in request handler: %s", e)
|
|
88
|
+
|
|
89
|
+
def subscribe_to_request_event(
|
|
90
|
+
self, success_handler: Callable[[Any, Any], None], failure_handler: Callable[[Any, Any], None]
|
|
91
|
+
) -> str:
|
|
92
|
+
"""Subscribe to a request-response event."""
|
|
93
|
+
request_id = str(uuid.uuid4())
|
|
94
|
+
self.request_handlers[request_id] = (success_handler, failure_handler)
|
|
95
|
+
return request_id
|
|
96
|
+
|
|
97
|
+
def unsubscribe_from_request_event(self, request_id: str) -> None:
|
|
98
|
+
"""Unsubscribe from a request-response event."""
|
|
99
|
+
if request_id in self.request_handlers:
|
|
100
|
+
del self.request_handlers[request_id]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class AsyncRequestManager(Generic[T]): # noqa: UP046
|
|
104
|
+
def __init__(self, connection_manager: WebSocketConnectionManager, api_key: str):
|
|
105
|
+
self.connection_manager = connection_manager
|
|
106
|
+
self.api_key = api_key
|
|
107
|
+
|
|
108
|
+
async def _subscribe_to_topic(self, topic: str) -> None:
|
|
109
|
+
"""Subscribe to a specific topic in the message bus."""
|
|
110
|
+
if self.connection_manager.websocket is None:
|
|
111
|
+
logger.warning("WebSocket connection not available for subscribing to topic")
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
body = {"type": "subscribe", "topic": topic, "payload": {}}
|
|
116
|
+
await self.connection_manager.websocket.send(json.dumps(body))
|
|
117
|
+
logger.debug("Subscribed to topic: %s", topic)
|
|
118
|
+
except websockets.exceptions.WebSocketException as e:
|
|
119
|
+
logger.error("Error subscribing to topic %s: %s", topic, e)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error("Unexpected error while subscribing to topic %s: %s", topic, e)
|
|
122
|
+
|
|
123
|
+
async def _subscribe_to_topics(self) -> None:
|
|
124
|
+
from griptape_nodes.retained_mode.managers.session_manager import SessionManager
|
|
125
|
+
from griptape_nodes.retained_mode.utils.engine_identity import EngineIdentity
|
|
126
|
+
|
|
127
|
+
# Subscribe to response topic (engine discovery)
|
|
128
|
+
await self._subscribe_to_topic("response")
|
|
129
|
+
|
|
130
|
+
# Get engine ID and subscribe to engine_id/response
|
|
131
|
+
engine_id = EngineIdentity.get_engine_id()
|
|
132
|
+
if engine_id:
|
|
133
|
+
await self._subscribe_to_topic(f"engines/{engine_id}/response")
|
|
134
|
+
else:
|
|
135
|
+
logger.warning("Engine ID not available for subscription")
|
|
136
|
+
|
|
137
|
+
# Get session ID and subscribe to session_id/response if available
|
|
138
|
+
session_id = SessionManager.get_saved_session_id()
|
|
139
|
+
if session_id:
|
|
140
|
+
topic = f"sessions/{session_id}/response"
|
|
141
|
+
await self._subscribe_to_topic(topic)
|
|
142
|
+
else:
|
|
143
|
+
logger.info("No session ID available for subscription")
|
|
144
|
+
|
|
145
|
+
async def connect(self) -> None:
|
|
146
|
+
"""Connect to the WebSocket server."""
|
|
147
|
+
from griptape_nodes.app.app import _create_websocket_connection
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
self.connection_manager.websocket = await _create_websocket_connection(self.api_key)
|
|
151
|
+
logger.debug("🟢 WebSocket connection established: %s", self.connection_manager.websocket)
|
|
152
|
+
|
|
153
|
+
await self._subscribe_to_topics()
|
|
154
|
+
|
|
155
|
+
# Start processing messages
|
|
156
|
+
self.connection_manager._process_task = asyncio.create_task(self.connection_manager._process_messages())
|
|
157
|
+
|
|
158
|
+
except Exception as e:
|
|
159
|
+
self.connection_manager.connected = False
|
|
160
|
+
logger.error("🔴 WebSocket connection failed: %s", str(e))
|
|
161
|
+
msg = f"Failed to connect to WebSocket: {e!s}"
|
|
162
|
+
raise ConnectionError(msg) from e
|
|
163
|
+
|
|
164
|
+
async def disconnect(self) -> None:
|
|
165
|
+
"""Disconnect from the WebSocket server."""
|
|
166
|
+
if self.connection_manager.websocket:
|
|
167
|
+
await self.connection_manager.websocket.close()
|
|
168
|
+
self.connection_manager.websocket = None
|
|
169
|
+
self.connection_manager.connected = False
|
|
170
|
+
|
|
171
|
+
# Cancel processing task if it's running
|
|
172
|
+
if self.connection_manager._process_task:
|
|
173
|
+
self.connection_manager._process_task.cancel()
|
|
174
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
175
|
+
await self.connection_manager._process_task
|
|
176
|
+
self.connection_manager._process_task = None
|
|
177
|
+
|
|
178
|
+
logger.debug("WebSocket disconnected")
|
|
179
|
+
|
|
180
|
+
async def send_api_message(self, data: dict[str, Any]) -> None:
|
|
181
|
+
"""Send a message to the API via WebSocket."""
|
|
182
|
+
try:
|
|
183
|
+
await self.connection_manager.send(data)
|
|
184
|
+
except ConnectionError as e:
|
|
185
|
+
logger.error("Failed to send API message: %s", e)
|
|
186
|
+
raise
|
|
187
|
+
except Exception as e:
|
|
188
|
+
logger.error("Unexpected error sending API message: %s", e)
|
|
189
|
+
raise
|
|
190
|
+
|
|
191
|
+
async def create_event(self, request_type: str, payload: dict[str, Any]) -> None:
|
|
192
|
+
"""Send an event to the API without waiting for a response."""
|
|
193
|
+
from griptape_nodes.app.app import _determine_request_topic
|
|
194
|
+
|
|
195
|
+
logger.debug("📝 Creating Event: %s - %s", request_type, json.dumps(payload))
|
|
196
|
+
|
|
197
|
+
data = {"event_type": "EventRequest", "request_type": request_type, "request": payload}
|
|
198
|
+
topic = _determine_request_topic()
|
|
199
|
+
|
|
200
|
+
request_data = {"payload": data, "type": data["event_type"], "topic": topic}
|
|
201
|
+
|
|
202
|
+
if not request_data["payload"]["request"].get("request_id"):
|
|
203
|
+
request_data["payload"]["request"]["request_id"] = ""
|
|
204
|
+
|
|
205
|
+
await self.send_api_message(request_data)
|
|
206
|
+
|
|
207
|
+
async def create_request_event(
|
|
208
|
+
self, request_type: str, payload: dict[str, Any], timeout_ms: int | None = None
|
|
209
|
+
) -> T:
|
|
210
|
+
"""Send a request and wait for its response.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
request_type: Type of request to send
|
|
214
|
+
payload: Data to send with the request
|
|
215
|
+
timeout_ms: Optional timeout in milliseconds
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
The response data
|
|
219
|
+
|
|
220
|
+
Raises:
|
|
221
|
+
asyncio.TimeoutError: If the request times out
|
|
222
|
+
Exception: If the request fails
|
|
223
|
+
"""
|
|
224
|
+
# Create a future that will be resolved when the response arrives
|
|
225
|
+
response_future = asyncio.Future()
|
|
226
|
+
|
|
227
|
+
# Convert timeout from milliseconds to seconds for asyncio
|
|
228
|
+
timeout_sec = timeout_ms / 1000 if timeout_ms else None
|
|
229
|
+
|
|
230
|
+
# Define handlers that will resolve/reject the future
|
|
231
|
+
def success_handler(response: Any, _: Any) -> None:
|
|
232
|
+
if not response_future.done():
|
|
233
|
+
result = response.get("payload", {}).get("result", "Success")
|
|
234
|
+
logger.debug("✅ Request succeeded: %s", result)
|
|
235
|
+
response_future.set_result(result)
|
|
236
|
+
|
|
237
|
+
def failure_handler(response: Any, _: Any) -> None:
|
|
238
|
+
if not response_future.done():
|
|
239
|
+
error = (
|
|
240
|
+
response.get("payload", {}).get("result", {}).get("exception", "Unknown error") or "Unknown error"
|
|
241
|
+
)
|
|
242
|
+
logger.error("❌ Request failed: %s", error)
|
|
243
|
+
response_future.set_exception(Exception(error))
|
|
244
|
+
|
|
245
|
+
# Generate request ID and subscribe
|
|
246
|
+
request_id = self.connection_manager.subscribe_to_request_event(success_handler, failure_handler)
|
|
247
|
+
payload["request_id"] = request_id
|
|
248
|
+
|
|
249
|
+
logger.debug("🚀 Request (%s): %s %s", request_id, request_type, json.dumps(payload))
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
# Send the event
|
|
253
|
+
await self.create_event(request_type, payload)
|
|
254
|
+
|
|
255
|
+
# Wait for the response with optional timeout
|
|
256
|
+
if timeout_sec:
|
|
257
|
+
return await asyncio.wait_for(response_future, timeout=timeout_sec)
|
|
258
|
+
return await response_future
|
|
259
|
+
|
|
260
|
+
except TimeoutError:
|
|
261
|
+
logger.error("Request timed out after %s ms: %s", timeout_ms, request_id)
|
|
262
|
+
self.connection_manager.unsubscribe_from_request_event(request_id)
|
|
263
|
+
raise
|
|
264
|
+
|
|
265
|
+
except Exception as e:
|
|
266
|
+
logger.error("Request failed: %s - %s", request_id, e)
|
|
267
|
+
self.connection_manager.unsubscribe_from_request_event(request_id)
|
|
268
|
+
raise
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
griptape_nodes/py.typed
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -82,6 +82,12 @@ class AppInitializationComplete(AppPayload):
|
|
|
82
82
|
"""Application initialization completed successfully. All subsystems ready."""
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
@dataclass
|
|
86
|
+
@PayloadRegistry.register
|
|
87
|
+
class AppConnectionEstablished(AppPayload):
|
|
88
|
+
"""Notification that a connection to the API has been established."""
|
|
89
|
+
|
|
90
|
+
|
|
85
91
|
@dataclass
|
|
86
92
|
@PayloadRegistry.register
|
|
87
93
|
class GetEngineVersionRequest(RequestPayload):
|
|
@@ -192,7 +198,6 @@ class EngineHeartbeatResultSuccess(ResultPayloadSuccess):
|
|
|
192
198
|
instance_region: Cloud instance region (None if not applicable)
|
|
193
199
|
instance_provider: Cloud provider name (None if not applicable)
|
|
194
200
|
deployment_type: Type of deployment (None if not applicable)
|
|
195
|
-
public_ip: Public IP address (None if not available)
|
|
196
201
|
current_workflow: Name of active workflow (None if none)
|
|
197
202
|
workflow_file_path: Path to workflow file (None if none)
|
|
198
203
|
has_active_flow: Whether there's an active flow running
|
|
@@ -208,7 +213,6 @@ class EngineHeartbeatResultSuccess(ResultPayloadSuccess):
|
|
|
208
213
|
instance_region: str | None
|
|
209
214
|
instance_provider: str | None
|
|
210
215
|
deployment_type: str | None
|
|
211
|
-
public_ip: str | None
|
|
212
216
|
current_workflow: str | None
|
|
213
217
|
workflow_file_path: str | None
|
|
214
218
|
has_active_flow: bool
|
|
File without changes
|