griptape-nodes 0.56.0__py3-none-any.whl → 0.56.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.
Files changed (39) hide show
  1. griptape_nodes/app/app.py +10 -15
  2. griptape_nodes/app/watch.py +35 -67
  3. griptape_nodes/bootstrap/utils/__init__.py +1 -0
  4. griptape_nodes/bootstrap/utils/python_subprocess_executor.py +122 -0
  5. griptape_nodes/bootstrap/workflow_executors/local_session_workflow_executor.py +418 -0
  6. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +37 -8
  7. griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +326 -0
  8. griptape_nodes/bootstrap/workflow_executors/utils/__init__.py +1 -0
  9. griptape_nodes/bootstrap/workflow_executors/utils/subprocess_script.py +51 -0
  10. griptape_nodes/bootstrap/workflow_publishers/__init__.py +1 -0
  11. griptape_nodes/bootstrap/workflow_publishers/local_workflow_publisher.py +43 -0
  12. griptape_nodes/bootstrap/workflow_publishers/subprocess_workflow_publisher.py +84 -0
  13. griptape_nodes/bootstrap/workflow_publishers/utils/__init__.py +1 -0
  14. griptape_nodes/bootstrap/workflow_publishers/utils/subprocess_script.py +54 -0
  15. griptape_nodes/cli/commands/engine.py +4 -15
  16. griptape_nodes/cli/main.py +6 -1
  17. griptape_nodes/exe_types/core_types.py +26 -0
  18. griptape_nodes/exe_types/node_types.py +116 -1
  19. griptape_nodes/retained_mode/events/agent_events.py +2 -0
  20. griptape_nodes/retained_mode/events/base_events.py +18 -17
  21. griptape_nodes/retained_mode/events/execution_events.py +3 -1
  22. griptape_nodes/retained_mode/events/flow_events.py +5 -7
  23. griptape_nodes/retained_mode/events/mcp_events.py +363 -0
  24. griptape_nodes/retained_mode/events/node_events.py +3 -4
  25. griptape_nodes/retained_mode/griptape_nodes.py +8 -0
  26. griptape_nodes/retained_mode/managers/agent_manager.py +67 -4
  27. griptape_nodes/retained_mode/managers/event_manager.py +31 -13
  28. griptape_nodes/retained_mode/managers/flow_manager.py +76 -44
  29. griptape_nodes/retained_mode/managers/library_manager.py +7 -9
  30. griptape_nodes/retained_mode/managers/mcp_manager.py +364 -0
  31. griptape_nodes/retained_mode/managers/node_manager.py +12 -1
  32. griptape_nodes/retained_mode/managers/settings.py +40 -0
  33. griptape_nodes/retained_mode/managers/workflow_manager.py +94 -8
  34. griptape_nodes/traits/multi_options.py +5 -1
  35. griptape_nodes/traits/options.py +10 -2
  36. {griptape_nodes-0.56.0.dist-info → griptape_nodes-0.56.1.dist-info}/METADATA +2 -2
  37. {griptape_nodes-0.56.0.dist-info → griptape_nodes-0.56.1.dist-info}/RECORD +39 -26
  38. {griptape_nodes-0.56.0.dist-info → griptape_nodes-0.56.1.dist-info}/WHEEL +0 -0
  39. {griptape_nodes-0.56.0.dist-info → griptape_nodes-0.56.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,363 @@
1
+ """MCP (Model Context Protocol) server management events."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, TypedDict
5
+
6
+ from griptape_nodes.retained_mode.events.base_events import (
7
+ RequestPayload,
8
+ ResultPayloadFailure,
9
+ ResultPayloadSuccess,
10
+ WorkflowAlteredMixin,
11
+ WorkflowNotAlteredMixin,
12
+ )
13
+ from griptape_nodes.retained_mode.events.payload_registry import PayloadRegistry
14
+
15
+
16
+ # Type definitions for better clarity
17
+ class MCPServerConfig(TypedDict, total=False):
18
+ """Configuration for an MCP server."""
19
+
20
+ name: str
21
+ transport: str # Transport protocol type
22
+ enabled: bool
23
+ command: str | None # Process command for stdio transport
24
+ args: list[str] | None # Process arguments for stdio transport
25
+ env: dict[str, str] | None # Environment variables for stdio transport
26
+ cwd: str | None # Working directory for stdio transport
27
+ encoding: str # Text encoding for stdio transport
28
+ encoding_error_handler: str # Error handling strategy for stdio transport
29
+ url: str | None # Connection URL for HTTP-based transports
30
+ headers: dict[str, str] | None # HTTP headers for HTTP-based transports
31
+ timeout: float | None # Connection timeout for HTTP-based transports
32
+ sse_read_timeout: float | None # Read timeout for SSE transport
33
+ terminate_on_close: bool # Session termination behavior for streamable HTTP transport
34
+ description: str | None
35
+ capabilities: list[str] | None
36
+
37
+
38
+ class MCPServerCapability(TypedDict):
39
+ """Information about an MCP server capability."""
40
+
41
+ name: str
42
+ description: str | None
43
+ input_schema: dict[str, Any] | None # JSON schema for capability inputs
44
+ output_schema: dict[str, Any] | None # JSON schema for capability outputs
45
+
46
+
47
+ class MCPServerInfo(TypedDict, total=False):
48
+ """Information about an MCP server."""
49
+
50
+ name: str
51
+ version: str | None # Server version identifier
52
+ description: str | None # Human-readable server description
53
+ capabilities: list[str] | None # List of supported capability names
54
+
55
+
56
+ # MCP Server Management Events
57
+
58
+
59
+ # Capability Discovery Events
60
+ @dataclass
61
+ @PayloadRegistry.register
62
+ class DiscoverMCPServerCapabilitiesRequest(RequestPayload):
63
+ """Discover capabilities from a running MCP server.
64
+
65
+ Args:
66
+ name: The MCP server identifier to discover capabilities from
67
+ timeout: Maximum time to wait for server response in seconds
68
+ """
69
+
70
+ name: str
71
+ timeout: int = 30
72
+
73
+
74
+ @dataclass
75
+ @PayloadRegistry.register
76
+ class DiscoverMCPServerCapabilitiesResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
77
+ """MCP server capabilities discovered successfully."""
78
+
79
+ name: str
80
+ capabilities: list[str]
81
+ detailed_tools: list[MCPServerCapability] | None = None
82
+ server_info: MCPServerInfo | None = None
83
+
84
+
85
+ @dataclass
86
+ @PayloadRegistry.register
87
+ class DiscoverMCPServerCapabilitiesResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
88
+ """Failed to discover MCP server capabilities."""
89
+
90
+
91
+ @dataclass
92
+ @PayloadRegistry.register
93
+ class ListMCPServersRequest(RequestPayload):
94
+ """List all configured MCP servers.
95
+
96
+ Args:
97
+ include_disabled: Whether to include disabled servers in the results
98
+ """
99
+
100
+ include_disabled: bool = False
101
+
102
+
103
+ @dataclass
104
+ @PayloadRegistry.register
105
+ class ListMCPServersResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
106
+ """MCP servers listed successfully."""
107
+
108
+ servers: dict[str, MCPServerConfig]
109
+
110
+
111
+ @dataclass
112
+ @PayloadRegistry.register
113
+ class ListMCPServersResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
114
+ """Failed to list MCP servers."""
115
+
116
+
117
+ @dataclass
118
+ @PayloadRegistry.register
119
+ class GetMCPServerRequest(RequestPayload):
120
+ """Get configuration for a specific MCP server.
121
+
122
+ Args:
123
+ name: The unique identifier for the MCP server
124
+ """
125
+
126
+ name: str
127
+
128
+
129
+ @dataclass
130
+ @PayloadRegistry.register
131
+ class GetMCPServerResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
132
+ """MCP server configuration retrieved successfully."""
133
+
134
+ server_config: MCPServerConfig
135
+
136
+
137
+ @dataclass
138
+ @PayloadRegistry.register
139
+ class GetMCPServerResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
140
+ """Failed to get MCP server configuration."""
141
+
142
+
143
+ @dataclass
144
+ @PayloadRegistry.register
145
+ class CreateMCPServerRequest(RequestPayload):
146
+ """Create a new MCP server configuration.
147
+
148
+ Args:
149
+ name: Unique identifier for the server
150
+ transport: Transport protocol type
151
+ command: Process command to start the server (required for stdio transport)
152
+ args: Process arguments to pass to the command (stdio transport)
153
+ env: Environment variables for the server process (stdio transport)
154
+ cwd: Working directory for the server process (stdio transport)
155
+ encoding: Text encoding for stdio communication
156
+ encoding_error_handler: Encoding error handling strategy for stdio
157
+ url: Connection URL for HTTP-based transports
158
+ headers: HTTP headers for HTTP-based connections
159
+ timeout: Connection timeout in seconds
160
+ sse_read_timeout: SSE read timeout in seconds
161
+ terminate_on_close: Session termination behavior for streamable HTTP transport
162
+ description: Optional description of the server
163
+ capabilities: List of server capabilities
164
+ enabled: Whether the server is enabled by default
165
+ """
166
+
167
+ name: str
168
+ transport: str = "stdio"
169
+ enabled: bool = True
170
+
171
+ # StdioConnection fields
172
+ command: str | None = None
173
+ args: list[str] | None = None
174
+ env: dict[str, str] | None = None
175
+ cwd: str | None = None
176
+ encoding: str = "utf-8"
177
+ encoding_error_handler: str = "strict"
178
+
179
+ # HTTP-based connection fields
180
+ url: str | None = None
181
+ headers: dict[str, str] | None = None
182
+ timeout: float | None = None
183
+ sse_read_timeout: float | None = None
184
+ terminate_on_close: bool = True
185
+
186
+ # Common fields
187
+ description: str | None = None
188
+ capabilities: list[str] | None = None
189
+
190
+
191
+ @dataclass
192
+ @PayloadRegistry.register
193
+ class CreateMCPServerResultSuccess(WorkflowAlteredMixin, ResultPayloadSuccess):
194
+ """MCP server created successfully."""
195
+
196
+ name: str
197
+
198
+
199
+ @dataclass
200
+ @PayloadRegistry.register
201
+ class CreateMCPServerResultFailure(WorkflowAlteredMixin, ResultPayloadFailure):
202
+ """Failed to create MCP server."""
203
+
204
+
205
+ @dataclass
206
+ @PayloadRegistry.register
207
+ class UpdateMCPServerRequest(RequestPayload):
208
+ """Update an existing MCP server configuration.
209
+
210
+ Args:
211
+ name: The unique identifier for the MCP server
212
+ new_name: Updated display name for the server
213
+ transport: Updated transport protocol type
214
+ command: Updated process command to start the server (stdio transport)
215
+ args: Updated process arguments to pass to the command (stdio transport)
216
+ env: Updated environment variables for the server process (stdio transport)
217
+ cwd: Updated working directory for the server process (stdio transport)
218
+ encoding: Updated text encoding for stdio communication
219
+ encoding_error_handler: Updated encoding error handling strategy for stdio
220
+ url: Updated connection URL for HTTP-based transports
221
+ headers: Updated HTTP headers for HTTP-based connections
222
+ timeout: Updated connection timeout in seconds
223
+ sse_read_timeout: Updated SSE read timeout in seconds
224
+ terminate_on_close: Updated session termination behavior for streamable HTTP transport
225
+ description: Updated description of the server
226
+ capabilities: Updated list of server capabilities
227
+ """
228
+
229
+ name: str
230
+ new_name: str | None = None
231
+ transport: str | None = None
232
+ enabled: bool | None = None
233
+
234
+ # StdioConnection fields
235
+ command: str | None = None
236
+ args: list[str] | None = None
237
+ env: dict[str, str] | None = None
238
+ cwd: str | None = None
239
+ encoding: str | None = None
240
+ encoding_error_handler: str | None = None
241
+
242
+ # HTTP-based connection fields
243
+ url: str | None = None
244
+ headers: dict[str, str] | None = None
245
+ timeout: float | None = None
246
+ sse_read_timeout: float | None = None
247
+ terminate_on_close: bool | None = None
248
+
249
+ # Common fields
250
+ description: str | None = None
251
+ capabilities: list[str] | None = None
252
+
253
+
254
+ @dataclass
255
+ @PayloadRegistry.register
256
+ class UpdateMCPServerResultSuccess(WorkflowAlteredMixin, ResultPayloadSuccess):
257
+ """MCP server updated successfully."""
258
+
259
+ name: str
260
+
261
+
262
+ @dataclass
263
+ @PayloadRegistry.register
264
+ class UpdateMCPServerResultFailure(WorkflowAlteredMixin, ResultPayloadFailure):
265
+ """Failed to update MCP server."""
266
+
267
+
268
+ @dataclass
269
+ @PayloadRegistry.register
270
+ class DeleteMCPServerRequest(RequestPayload):
271
+ """Delete an MCP server configuration.
272
+
273
+ Args:
274
+ name: The unique identifier for the MCP server to delete
275
+ """
276
+
277
+ name: str
278
+
279
+
280
+ @dataclass
281
+ @PayloadRegistry.register
282
+ class DeleteMCPServerResultSuccess(WorkflowAlteredMixin, ResultPayloadSuccess):
283
+ """MCP server deleted successfully."""
284
+
285
+ name: str
286
+
287
+
288
+ @dataclass
289
+ @PayloadRegistry.register
290
+ class DeleteMCPServerResultFailure(WorkflowAlteredMixin, ResultPayloadFailure):
291
+ """Failed to delete MCP server."""
292
+
293
+
294
+ @dataclass
295
+ @PayloadRegistry.register
296
+ class EnableMCPServerRequest(RequestPayload):
297
+ """Enable an MCP server.
298
+
299
+ Args:
300
+ name: The unique identifier for the MCP server to enable
301
+ """
302
+
303
+ name: str
304
+
305
+
306
+ @dataclass
307
+ @PayloadRegistry.register
308
+ class EnableMCPServerResultSuccess(WorkflowAlteredMixin, ResultPayloadSuccess):
309
+ """MCP server enabled successfully."""
310
+
311
+ name: str
312
+
313
+
314
+ @dataclass
315
+ @PayloadRegistry.register
316
+ class EnableMCPServerResultFailure(WorkflowAlteredMixin, ResultPayloadFailure):
317
+ """Failed to enable MCP server."""
318
+
319
+
320
+ @dataclass
321
+ @PayloadRegistry.register
322
+ class DisableMCPServerRequest(RequestPayload):
323
+ """Disable an MCP server.
324
+
325
+ Args:
326
+ name: The unique identifier for the MCP server to disable
327
+ """
328
+
329
+ name: str
330
+
331
+
332
+ @dataclass
333
+ @PayloadRegistry.register
334
+ class DisableMCPServerResultSuccess(WorkflowAlteredMixin, ResultPayloadSuccess):
335
+ """MCP server disabled successfully."""
336
+
337
+ name: str
338
+
339
+
340
+ @dataclass
341
+ @PayloadRegistry.register
342
+ class DisableMCPServerResultFailure(WorkflowAlteredMixin, ResultPayloadFailure):
343
+ """Failed to disable MCP server."""
344
+
345
+
346
+ @dataclass
347
+ @PayloadRegistry.register
348
+ class GetEnabledMCPServersRequest(RequestPayload):
349
+ """Get all enabled MCP servers."""
350
+
351
+
352
+ @dataclass
353
+ @PayloadRegistry.register
354
+ class GetEnabledMCPServersResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
355
+ """Enabled MCP servers retrieved successfully."""
356
+
357
+ servers: dict[str, MCPServerConfig]
358
+
359
+
360
+ @dataclass
361
+ @PayloadRegistry.register
362
+ class GetEnabledMCPServersResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
363
+ """Failed to get enabled MCP servers."""
@@ -4,8 +4,7 @@ from typing import Any, NamedTuple, NewType
4
4
  from uuid import uuid4
5
5
 
6
6
  from griptape_nodes.exe_types.core_types import NodeMessagePayload
7
- from griptape_nodes.exe_types.node_types import NodeResolutionState
8
- from griptape_nodes.node_library.library_registry import LibraryNameAndVersion
7
+ from griptape_nodes.exe_types.node_types import NodeDependencies, NodeResolutionState
9
8
  from griptape_nodes.retained_mode.events.base_events import (
10
9
  RequestPayload,
11
10
  ResultPayloadFailure,
@@ -409,7 +408,7 @@ class SerializedNodeCommands:
409
408
  Attributes:
410
409
  create_node_command (CreateNodeRequest): The command to create the node.
411
410
  element_modification_commands (list[RequestPayload]): A list of commands to create or modify the elements (including Parameters) of the node.
412
- node_library_details (LibraryNameAndVersion): Details of the library and version used by the node.
411
+ node_dependencies (NodeDependencies): Dependencies that this node has on external resources (workflows, files, imports, libraries).
413
412
  node_uuid (NodeUUID): The UUID of this particular node. During deserialization, this UUID will be used to correlate this node's instance
414
413
  with the connections and parameter values necessary. We cannot use node name because Griptape Nodes enforces unique names, and we cannot
415
414
  predict the name that will be selected upon instantiation. Similarly, the same serialized node may be deserialized multiple times, such
@@ -435,7 +434,7 @@ class SerializedNodeCommands:
435
434
 
436
435
  create_node_command: CreateNodeRequest
437
436
  element_modification_commands: list[RequestPayload]
438
- node_library_details: LibraryNameAndVersion
437
+ node_dependencies: NodeDependencies
439
438
  lock_node_command: SetLockNodeStateRequest | None = None
440
439
  node_uuid: NodeUUID = field(default_factory=lambda: SerializedNodeCommands.NodeUUID(str(uuid4())))
441
440
 
@@ -62,6 +62,7 @@ if TYPE_CHECKING:
62
62
  from griptape_nodes.retained_mode.managers.event_manager import EventManager
63
63
  from griptape_nodes.retained_mode.managers.flow_manager import FlowManager
64
64
  from griptape_nodes.retained_mode.managers.library_manager import LibraryManager
65
+ from griptape_nodes.retained_mode.managers.mcp_manager import MCPManager
65
66
  from griptape_nodes.retained_mode.managers.model_manager import ModelManager
66
67
  from griptape_nodes.retained_mode.managers.node_manager import NodeManager
67
68
  from griptape_nodes.retained_mode.managers.object_manager import ObjectManager
@@ -150,6 +151,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
150
151
  _version_compatibility_manager: VersionCompatibilityManager
151
152
  _session_manager: SessionManager
152
153
  _engine_identity_manager: EngineIdentityManager
154
+ _mcp_manager: MCPManager
153
155
  _resource_manager: ResourceManager
154
156
  _sync_manager: SyncManager
155
157
 
@@ -164,6 +166,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
164
166
  from griptape_nodes.retained_mode.managers.event_manager import EventManager
165
167
  from griptape_nodes.retained_mode.managers.flow_manager import FlowManager
166
168
  from griptape_nodes.retained_mode.managers.library_manager import LibraryManager
169
+ from griptape_nodes.retained_mode.managers.mcp_manager import MCPManager
167
170
  from griptape_nodes.retained_mode.managers.model_manager import ModelManager
168
171
  from griptape_nodes.retained_mode.managers.node_manager import NodeManager
169
172
  from griptape_nodes.retained_mode.managers.object_manager import ObjectManager
@@ -212,6 +215,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
212
215
  self._version_compatibility_manager = VersionCompatibilityManager(self._event_manager)
213
216
  self._session_manager = SessionManager(self._event_manager)
214
217
  self._engine_identity_manager = EngineIdentityManager(self._event_manager)
218
+ self._mcp_manager = MCPManager(self._event_manager, self._config_manager)
215
219
  self._sync_manager = SyncManager(self._event_manager, self._config_manager)
216
220
 
217
221
  # Assign handlers now that these are created.
@@ -374,6 +378,10 @@ class GriptapeNodes(metaclass=SingletonMeta):
374
378
  def SessionManager(cls) -> SessionManager:
375
379
  return GriptapeNodes.get_instance()._session_manager
376
380
 
381
+ @classmethod
382
+ def MCPManager(cls) -> MCPManager:
383
+ return GriptapeNodes.get_instance()._mcp_manager
384
+
377
385
  @classmethod
378
386
  def EngineIdentityManager(cls) -> EngineIdentityManager:
379
387
  return GriptapeNodes.get_instance()._engine_identity_manager
@@ -4,7 +4,7 @@ import logging
4
4
  import os
5
5
  import threading
6
6
  import uuid
7
- from typing import TYPE_CHECKING
7
+ from typing import TYPE_CHECKING, ClassVar
8
8
 
9
9
  from attrs import define, field
10
10
  from griptape.artifacts import ErrorArtifact, ImageUrlArtifact, JsonArtifact
@@ -40,6 +40,10 @@ from griptape_nodes.retained_mode.events.agent_events import (
40
40
  )
41
41
  from griptape_nodes.retained_mode.events.app_events import AppInitializationComplete
42
42
  from griptape_nodes.retained_mode.events.base_events import ExecutionEvent, ExecutionGriptapeNodeEvent, ResultPayload
43
+ from griptape_nodes.retained_mode.events.mcp_events import (
44
+ GetEnabledMCPServersRequest,
45
+ GetEnabledMCPServersResultSuccess,
46
+ )
43
47
  from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
44
48
  from griptape_nodes.retained_mode.managers.config_manager import ConfigManager
45
49
  from griptape_nodes.retained_mode.managers.event_manager import EventManager
@@ -91,6 +95,14 @@ class NodesPromptImageGenerationTool(BaseImageGenerationTool):
91
95
 
92
96
 
93
97
  class AgentManager:
98
+ # Field mappings for each transport type
99
+ TRANSPORT_FIELD_MAPPINGS: ClassVar[dict[str, list[str]]] = {
100
+ "stdio": ["command", "args", "env", "cwd", "encoding", "encoding_error_handler"],
101
+ "sse": ["url", "headers", "timeout", "sse_read_timeout"],
102
+ "streamable_http": ["url", "headers", "timeout", "sse_read_timeout", "terminate_on_close"],
103
+ "websocket": ["url"],
104
+ }
105
+
94
106
  def __init__(self, static_files_manager: StaticFilesManager, event_manager: EventManager | None = None) -> None:
95
107
  self.conversation_memory = ConversationMemory()
96
108
  self.prompt_driver = None
@@ -135,7 +147,53 @@ class AgentManager:
135
147
  "transport": "streamable_http",
136
148
  "url": f"http://localhost:{GTN_MCP_SERVER_PORT}/mcp/",
137
149
  }
138
- return MCPTool(connection=connection)
150
+ return MCPTool(connection=connection, name="mcpGriptapeNodes")
151
+
152
+ def _create_additional_mcp_tools(self, server_names: list[str]) -> list[MCPTool]:
153
+ """Create MCP tools for additional servers specified in the request."""
154
+ additional_tools = []
155
+
156
+ try:
157
+ app = GriptapeNodes()
158
+
159
+ enabled_request = GetEnabledMCPServersRequest()
160
+ enabled_result = app.handle_request(enabled_request)
161
+
162
+ if not isinstance(enabled_result, GetEnabledMCPServersResultSuccess):
163
+ msg = f"Failed to get enabled MCP servers for additional tools: {enabled_result}. Agent will continue with default MCP tool only."
164
+ logger.warning(msg)
165
+ return additional_tools
166
+
167
+ for server_name in server_names:
168
+ if server_name in enabled_result.servers:
169
+ server_config = enabled_result.servers[server_name]
170
+ connection = self._create_connection_from_mcp_config(server_config) # type: ignore[arg-type]
171
+ tool = MCPTool(connection=connection, name=f"mcp{server_name.title()}") # type: ignore[arg-type]
172
+ additional_tools.append(tool)
173
+ else:
174
+ msg = f"Additional MCP server '{server_name}' not found or not enabled"
175
+ logger.warning(msg)
176
+
177
+ except Exception as e:
178
+ msg = f"Failed to create additional MCP tools: {e}"
179
+ logger.error(msg)
180
+
181
+ return additional_tools
182
+
183
+ def _create_connection_from_mcp_config(self, server_config: dict) -> dict:
184
+ """Create connection dictionary from MCP server configuration."""
185
+ transport = server_config.get("transport", "stdio")
186
+
187
+ # Start with transport
188
+ connection = {"transport": transport}
189
+
190
+ # Map relevant fields based on transport type
191
+ fields_to_map = self.TRANSPORT_FIELD_MAPPINGS.get(transport, self.TRANSPORT_FIELD_MAPPINGS["stdio"])
192
+ for field_name in fields_to_map:
193
+ if field_name in server_config and server_config[field_name] is not None:
194
+ connection[field_name] = server_config[field_name]
195
+
196
+ return connection
139
197
 
140
198
  async def on_handle_run_agent_request(self, request: RunAgentRequest) -> ResultPayload:
141
199
  if self.prompt_driver is None:
@@ -147,7 +205,7 @@ class AgentManager:
147
205
  await asyncio.to_thread(self._on_handle_run_agent_request, request)
148
206
  return RunAgentResultStarted(result_details="Agent execution started successfully.")
149
207
 
150
- def _create_agent(self) -> Agent:
208
+ def _create_agent(self, additional_mcp_servers: list[str] | None = None) -> Agent:
151
209
  output_schema = Schema(
152
210
  {
153
211
  "generated_image_urls": [str],
@@ -161,6 +219,11 @@ class AgentManager:
161
219
  if self.mcp_tool is not None:
162
220
  tools.append(self.mcp_tool)
163
221
 
222
+ # Add additional MCP servers if specified
223
+ if additional_mcp_servers:
224
+ additional_tools = self._create_additional_mcp_tools(additional_mcp_servers)
225
+ tools.extend(additional_tools)
226
+
164
227
  return Agent(
165
228
  prompt_driver=self.prompt_driver,
166
229
  conversation_memory=self.conversation_memory,
@@ -185,7 +248,7 @@ class AgentManager:
185
248
  for url_artifact in request.url_artifacts
186
249
  if url_artifact["type"] == "ImageUrlArtifact"
187
250
  ]
188
- agent = self._create_agent()
251
+ agent = self._create_agent(additional_mcp_servers=request.additional_mcp_servers)
189
252
  *events, last_event = agent.run_stream([request.input, *artifacts])
190
253
  full_result = ""
191
254
  last_conversation_output = ""
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import inspect
5
+ import threading
5
6
  from collections import defaultdict
6
7
  from dataclasses import fields
7
8
  from typing import TYPE_CHECKING, Any, cast
@@ -43,6 +44,10 @@ class EventManager:
43
44
  self._flush_in_queue: bool = False
44
45
  # Event queue for publishing events
45
46
  self._event_queue: asyncio.Queue | None = None
47
+ # Keep track of which thread the event loop runs on
48
+ self._loop_thread_id: int | None = None
49
+ # Keep a reference to the event loop for thread-safe operations
50
+ self._event_loop: asyncio.AbstractEventLoop | None = None
46
51
 
47
52
  @property
48
53
  def event_queue(self) -> asyncio.Queue:
@@ -62,23 +67,48 @@ class EventManager:
62
67
  """
63
68
  if queue is not None:
64
69
  self._event_queue = queue
70
+ # Track which thread the event loop is running on and store loop reference
71
+ try:
72
+ self._event_loop = asyncio.get_running_loop()
73
+ self._loop_thread_id = threading.get_ident()
74
+ except RuntimeError:
75
+ self._event_loop = None
76
+ self._loop_thread_id = None
65
77
  else:
66
78
  try:
67
79
  self._event_queue = asyncio.Queue()
80
+ self._event_loop = asyncio.get_running_loop()
81
+ self._loop_thread_id = threading.get_ident()
68
82
  except RuntimeError:
69
83
  # Defer queue creation until we're in an event loop
70
84
  self._event_queue = None
85
+ self._event_loop = None
86
+ self._loop_thread_id = None
71
87
 
72
88
  def put_event(self, event: Any) -> None:
73
89
  """Put event into async queue from sync context (non-blocking).
74
90
 
91
+ Automatically detects if we're in a different thread and uses thread-safe operations.
92
+
75
93
  Args:
76
94
  event: The event to publish to the queue
77
95
  """
78
96
  if self._event_queue is None:
79
97
  return
80
98
 
81
- self._event_queue.put_nowait(event)
99
+ # Check if we need thread-safe operation
100
+ current_thread_id = threading.get_ident()
101
+
102
+ if (
103
+ self._loop_thread_id is not None
104
+ and current_thread_id != self._loop_thread_id
105
+ and self._event_loop is not None
106
+ ):
107
+ # We're in a different thread from the event loop, use thread-safe method
108
+ self._event_loop.call_soon_threadsafe(self._event_queue.put_nowait, event)
109
+ else:
110
+ # We're on the same thread as the event loop or no loop thread tracked, use direct method
111
+ self._event_queue.put_nowait(event)
82
112
 
83
113
  async def aput_event(self, event: Any) -> None:
84
114
  """Put event into async queue from async context.
@@ -91,18 +121,6 @@ class EventManager:
91
121
 
92
122
  await self._event_queue.put(event)
93
123
 
94
- def put_event_threadsafe(self, loop: Any, event: Any) -> None:
95
- """Put event into async queue from sync context in a thread-safe manner.
96
-
97
- Args:
98
- loop: The asyncio event loop to use for thread-safe operation
99
- event: The event to publish to the queue
100
- """
101
- if self._event_queue is None:
102
- return
103
-
104
- loop.call_soon_threadsafe(self._event_queue.put_nowait, event)
105
-
106
124
  def assign_manager_to_request_type(
107
125
  self,
108
126
  request_type: type[RP],