flock-core 0.4.3__py3-none-any.whl → 0.4.503__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.

Potentially problematic release.


This version of flock-core might be problematic. Click here for more details.

Files changed (45) hide show
  1. flock/cli/create_flock.py +0 -7
  2. flock/cli/execute_flock.py +19 -9
  3. flock/core/__init__.py +11 -0
  4. flock/core/flock.py +145 -80
  5. flock/core/flock_agent.py +117 -4
  6. flock/core/flock_evaluator.py +1 -1
  7. flock/core/flock_factory.py +290 -2
  8. flock/core/flock_module.py +101 -0
  9. flock/core/flock_registry.py +40 -3
  10. flock/core/flock_server_manager.py +136 -0
  11. flock/core/logging/__init__.py +4 -0
  12. flock/core/logging/logging.py +98 -11
  13. flock/core/logging/telemetry.py +1 -1
  14. flock/core/mcp/__init__.py +1 -0
  15. flock/core/mcp/flock_mcp_server.py +614 -0
  16. flock/core/mcp/flock_mcp_tool_base.py +201 -0
  17. flock/core/mcp/mcp_client.py +658 -0
  18. flock/core/mcp/mcp_client_manager.py +201 -0
  19. flock/core/mcp/mcp_config.py +237 -0
  20. flock/core/mcp/types/__init__.py +1 -0
  21. flock/core/mcp/types/callbacks.py +86 -0
  22. flock/core/mcp/types/factories.py +111 -0
  23. flock/core/mcp/types/handlers.py +240 -0
  24. flock/core/mcp/types/types.py +157 -0
  25. flock/core/mcp/util/__init__.py +0 -0
  26. flock/core/mcp/util/helpers.py +23 -0
  27. flock/core/mixin/dspy_integration.py +45 -12
  28. flock/core/serialization/flock_serializer.py +52 -1
  29. flock/core/util/hydrator.py +0 -1
  30. flock/core/util/spliter.py +4 -0
  31. flock/evaluators/declarative/declarative_evaluator.py +4 -3
  32. flock/mcp/servers/sse/__init__.py +1 -0
  33. flock/mcp/servers/sse/flock_sse_server.py +139 -0
  34. flock/mcp/servers/stdio/__init__.py +1 -0
  35. flock/mcp/servers/stdio/flock_stdio_server.py +138 -0
  36. flock/mcp/servers/websockets/__init__.py +1 -0
  37. flock/mcp/servers/websockets/flock_websocket_server.py +119 -0
  38. flock/modules/performance/metrics_module.py +159 -1
  39. flock/webapp/app/main.py +1 -1
  40. flock/webapp/app/services/flock_service.py +0 -1
  41. {flock_core-0.4.3.dist-info → flock_core-0.4.503.dist-info}/METADATA +42 -4
  42. {flock_core-0.4.3.dist-info → flock_core-0.4.503.dist-info}/RECORD +45 -25
  43. {flock_core-0.4.3.dist-info → flock_core-0.4.503.dist-info}/WHEEL +0 -0
  44. {flock_core-0.4.3.dist-info → flock_core-0.4.503.dist-info}/entry_points.txt +0 -0
  45. {flock_core-0.4.3.dist-info → flock_core-0.4.503.dist-info}/licenses/LICENSE +0 -0
@@ -1,14 +1,49 @@
1
1
  """Factory for creating pre-configured Flock agents."""
2
2
 
3
+ import os
3
4
  from collections.abc import Callable
4
- from typing import Any
5
+ from pathlib import Path
6
+ from typing import Any, Literal
7
+
8
+ from pydantic import AnyUrl, BaseModel, Field, FileUrl
5
9
 
6
10
  from flock.core.flock_agent import FlockAgent, SignatureType
7
11
  from flock.core.logging.formatters.themes import OutputTheme
12
+ from flock.core.mcp.flock_mcp_server import FlockMCPServerBase
13
+ from flock.core.mcp.mcp_config import (
14
+ FlockMCPCachingConfigurationBase,
15
+ FlockMCPCallbackConfigurationBase,
16
+ FlockMCPFeatureConfigurationBase,
17
+ )
18
+ from flock.core.mcp.types.types import (
19
+ FlockListRootsMCPCallback,
20
+ FlockLoggingMCPCallback,
21
+ FlockMessageHandlerMCPCallback,
22
+ FlockSamplingMCPCallback,
23
+ MCPRoot,
24
+ SseServerParameters,
25
+ StdioServerParameters,
26
+ WebsocketServerParameters,
27
+ )
8
28
  from flock.evaluators.declarative.declarative_evaluator import (
9
29
  DeclarativeEvaluator,
10
30
  DeclarativeEvaluatorConfig,
11
31
  )
32
+ from flock.mcp.servers.sse.flock_sse_server import (
33
+ FlockSSEConfig,
34
+ FlockSSEConnectionConfig,
35
+ FlockSSEServer,
36
+ )
37
+ from flock.mcp.servers.stdio.flock_stdio_server import (
38
+ FlockMCPStdioServer,
39
+ FlockStdioConfig,
40
+ FlockStdioConnectionConfig,
41
+ )
42
+ from flock.mcp.servers.websockets.flock_websocket_server import (
43
+ FlockWSConfig,
44
+ FlockWSConnectionConfig,
45
+ FlockWSServer,
46
+ )
12
47
  from flock.modules.output.output_module import OutputModule, OutputModuleConfig
13
48
  from flock.modules.performance.metrics_module import (
14
49
  MetricsModule,
@@ -16,9 +51,260 @@ from flock.modules.performance.metrics_module import (
16
51
  )
17
52
  from flock.workflow.temporal_config import TemporalActivityConfig
18
53
 
54
+ LoggingLevel = Literal[
55
+ "debug",
56
+ "info",
57
+ "notice",
58
+ "warning",
59
+ "error",
60
+ "critical",
61
+ "alert",
62
+ "emergency",
63
+ ]
64
+
19
65
 
20
66
  class FlockFactory:
21
- """Factory for creating pre-configured Flock agents with common module setups."""
67
+ """Factory for creating pre-configured Flock agents and pre-configured Flock MCPServers with common module setups."""
68
+
69
+ # Classes for type-hints.
70
+ class StdioParams(BaseModel):
71
+ """Factory-Params for Stdio-Servers."""
72
+
73
+ command: str = Field(
74
+ ...,
75
+ description="Command for starting the local script. (e.g. 'uvx', 'bun', 'npx', 'bunx', etc.)",
76
+ )
77
+
78
+ args: list[str] = Field(
79
+ ...,
80
+ description="Arguments for starting the local script. (e.g. ['run', './mcp-server.py'])",
81
+ )
82
+
83
+ env: dict[str, Any] | None = Field(
84
+ default=None,
85
+ description="Environment variables to pass to the server. (e.g. {'GOOGLE_API_KEY': 'MY_SUPER_SECRET_API_KEY'})",
86
+ )
87
+
88
+ cwd: str | Path | None = Field(
89
+ default_factory=os.getcwd,
90
+ description="The working directory to start the script in.",
91
+ )
92
+
93
+ encoding: str = Field(
94
+ default="utf-8",
95
+ description="The char-encoding to use when talking to a stdio server. (e.g. 'utf-8', 'ascii', etc.)",
96
+ )
97
+
98
+ encoding_error_handler: Literal["strict", "ignore", "replace"] = Field(
99
+ default="strict",
100
+ description="The text encoding error handler. See https://docs.python.org/3/library/codecs.html#codec-base-classes for explanations of possible values",
101
+ )
102
+
103
+ class SSEParams(BaseModel):
104
+ """Factory-Params for SSE-Servers."""
105
+
106
+ url: str | AnyUrl = Field(
107
+ ...,
108
+ description="Url the server listens at. (e.g. https://my-mcp-server.io/sse)",
109
+ )
110
+
111
+ headers: dict[str, Any] | None = Field(
112
+ default=None,
113
+ description="Additional Headers to pass to the client.",
114
+ )
115
+
116
+ timeout_seconds: float | int = Field(
117
+ default=5, description="Http Timeout in Seconds."
118
+ )
119
+
120
+ sse_read_timeout_seconds: float | int = Field(
121
+ default=60 * 5,
122
+ description="How many seconds to wait for server-sent events until closing the connection. (connections will be automatically re-established.)",
123
+ )
124
+
125
+ class WebsocketParams(BaseModel):
126
+ """Factory-Params for Websocket Servers."""
127
+
128
+ url: str | AnyUrl = Field(
129
+ ...,
130
+ description="The url the server listens at. (e.g. ws://my-mcp-server.io/messages)",
131
+ )
132
+
133
+ @staticmethod
134
+ def create_mcp_server(
135
+ name: str,
136
+ connection_params: SSEParams | StdioParams | WebsocketParams,
137
+ max_retries: int = 3,
138
+ mount_points: list[str | MCPRoot] | None = None,
139
+ timeout_seconds: int | float = 10,
140
+ server_logging_level: LoggingLevel = "error",
141
+ enable_roots_feature: bool = False,
142
+ enable_tools_feature: bool = False,
143
+ enable_sampling_feature: bool = False,
144
+ enable_prompts_feature: bool = False,
145
+ sampling_callback: FlockSamplingMCPCallback | None = None,
146
+ list_roots_callback: FlockListRootsMCPCallback | None = None,
147
+ logging_callback: FlockLoggingMCPCallback | None = None,
148
+ message_handler: FlockMessageHandlerMCPCallback | None = None,
149
+ tool_cache_size: float = 100,
150
+ tool_cache_ttl: float = 60,
151
+ resource_contents_cache_size=10,
152
+ resource_contents_cache_ttl=60 * 5,
153
+ resource_list_cache_size=100,
154
+ resource_list_cache_ttl=100,
155
+ tool_result_cache_size=100,
156
+ tool_result_cache_ttl=100,
157
+ description: str | Callable[..., str] | None = None,
158
+ alert_latency_threshold_ms: int = 30000,
159
+ ) -> FlockMCPServerBase:
160
+ """Create a default MCP Server with common modules.
161
+
162
+ Allows for creating one of the three default-implementations provided
163
+ by Flock:
164
+ - SSE-Server (specify "sse" in type)
165
+ - Stdio-Server (specify "stdio" in type)
166
+ - Websockets-Server (specifiy "websockets" in type)
167
+ """
168
+ # infer server type from the pydantic model class
169
+ if isinstance(connection_params, FlockFactory.StdioParams):
170
+ server_kind = "stdio"
171
+ concrete_server_cls = FlockMCPStdioServer
172
+ if isinstance(connection_params, FlockFactory.SSEParams):
173
+ server_kind = "sse"
174
+ concrete_server_cls = FlockSSEServer
175
+ if isinstance(connection_params, FlockFactory.WebsocketParams):
176
+ server_kind = "websockets"
177
+ concrete_server_cls = FlockWSServer
178
+
179
+ # convert mount points.
180
+ mounts: list[MCPRoot] = []
181
+ if mount_points:
182
+ for item in mount_points:
183
+ if isinstance(item, MCPRoot):
184
+ mounts.append(item)
185
+ elif isinstance(item, str):
186
+ try:
187
+ conv = MCPRoot(uri=FileUrl(url=item))
188
+ mounts.append(conv)
189
+ except Exception:
190
+ continue # ignore
191
+ else:
192
+ continue # ignore
193
+
194
+ # build generic configs
195
+ feature_config = FlockMCPFeatureConfigurationBase(
196
+ roots_enabled=enable_roots_feature,
197
+ tools_enabled=enable_tools_feature,
198
+ prompts_enabled=enable_prompts_feature,
199
+ sampling_enabled=enable_sampling_feature,
200
+ )
201
+ callback_config = FlockMCPCallbackConfigurationBase(
202
+ sampling_callback=sampling_callback,
203
+ list_roots_callback=list_roots_callback,
204
+ logging_callback=logging_callback,
205
+ message_handler=message_handler,
206
+ )
207
+ caching_config = FlockMCPCachingConfigurationBase(
208
+ tool_cache_max_size=tool_cache_size,
209
+ tool_cache_max_ttl=tool_cache_ttl,
210
+ resource_contents_cache_max_size=resource_contents_cache_size,
211
+ resource_contents_cache_max_ttl=resource_contents_cache_ttl,
212
+ resource_list_cache_max_size=resource_list_cache_size,
213
+ resource_list_cache_max_ttl=resource_list_cache_ttl,
214
+ tool_result_cache_max_size=tool_result_cache_size,
215
+ tool_result_cache_max_ttl=tool_result_cache_ttl,
216
+ )
217
+ connection_config = None
218
+ server_config: (
219
+ FlockStdioConfig | FlockSSEConfig | FlockWSConfig | None
220
+ ) = None
221
+
222
+ # Instantiate correct server + config
223
+ if server_kind == "stdio":
224
+ # build stdio config
225
+ connection_config = FlockStdioConnectionConfig(
226
+ max_retries=max_retries,
227
+ connection_parameters=StdioServerParameters(
228
+ command=connection_params.command,
229
+ args=connection_params.args,
230
+ env=connection_params.env,
231
+ encoding=connection_params.encoding,
232
+ encoding_error_handler=connection_params.encoding_error_handler,
233
+ cwd=connection_params.cwd,
234
+ ),
235
+ mount_points=mounts,
236
+ read_timeout_seconds=timeout_seconds,
237
+ server_logging_level=server_logging_level,
238
+ )
239
+ server_config = FlockStdioConfig(
240
+ name=name,
241
+ connection_config=connection_config,
242
+ feature_config=feature_config,
243
+ caching_config=caching_config,
244
+ callback_config=callback_config,
245
+ )
246
+ elif server_kind == "sse":
247
+ # build sse config
248
+ connection_config = FlockSSEConnectionConfig(
249
+ max_retries=max_retries,
250
+ connection_parameters=SseServerParameters(
251
+ url=connection_params.url,
252
+ headers=connection_params.headers,
253
+ timeout=connection_params.timeout_seconds,
254
+ sse_read_timeout=connection_params.sse_read_timeout_seconds,
255
+ ),
256
+ mount_points=mounts,
257
+ server_logging_level=server_logging_level,
258
+ )
259
+
260
+ server_config = FlockSSEConfig(
261
+ name=name,
262
+ connection_config=connection_config,
263
+ feature_config=feature_config,
264
+ caching_config=caching_config,
265
+ callback_config=callback_config,
266
+ )
267
+
268
+ elif server_kind == "websockets":
269
+ # build websocket config
270
+ connection_config = FlockWSConnectionConfig(
271
+ max_retries=max_retries,
272
+ connection_parameters=WebsocketServerParameters(
273
+ url=connection_params.url,
274
+ ),
275
+ mount_points=mounts,
276
+ server_logging_level=server_logging_level,
277
+ )
278
+
279
+ server_config = FlockWSConfig(
280
+ name=name,
281
+ connection_config=connection_config,
282
+ feature_config=feature_config,
283
+ caching_config=caching_config,
284
+ callback_config=callback_config,
285
+ )
286
+
287
+ else:
288
+ raise ValueError(
289
+ f"Unsupported connection_params type: {type(connection_params)}"
290
+ )
291
+
292
+ if not server_config:
293
+ raise ValueError(
294
+ f"Unable to create server configuration for passed params."
295
+ )
296
+
297
+ server = concrete_server_cls(config=server_config)
298
+
299
+ metrics_module_config = MetricsModuleConfig(
300
+ latency_threshold_ms=alert_latency_threshold_ms
301
+ )
302
+
303
+ metrics_module = MetricsModule("metrics", config=metrics_module_config)
304
+
305
+ server.add_module(metrics_module)
306
+
307
+ return server
22
308
 
23
309
  @staticmethod
24
310
  def create_default_agent(
@@ -28,6 +314,7 @@ class FlockFactory:
28
314
  input: SignatureType = None,
29
315
  output: SignatureType = None,
30
316
  tools: list[Callable[..., Any] | Any] | None = None,
317
+ servers: list[str | FlockMCPServerBase] | None = None,
31
318
  use_cache: bool = True,
32
319
  enable_rich_tables: bool = False,
33
320
  output_theme: OutputTheme = OutputTheme.abernathy,
@@ -66,6 +353,7 @@ class FlockFactory:
66
353
  input=input,
67
354
  output=output,
68
355
  tools=tools,
356
+ servers=servers,
69
357
  model=model,
70
358
  description=description,
71
359
  evaluator=evaluator,
@@ -104,3 +104,104 @@ class FlockModule(BaseModel, ABC):
104
104
  ) -> None:
105
105
  """Called when an error occurs during agent execution."""
106
106
  pass
107
+
108
+ async def on_pre_server_init(self, server: Any) -> None:
109
+ """Called before a server initializes."""
110
+ pass
111
+
112
+ async def on_post_server_init(self, server: Any) -> None:
113
+ """Called after a server initialized."""
114
+ pass
115
+
116
+ async def on_pre_server_terminate(self, server: Any) -> None:
117
+ """Called before a server terminates."""
118
+ pass
119
+
120
+ async def on_post_server_terminate(self, server: Any) -> None:
121
+ """Called after a server terminates."""
122
+ pass
123
+
124
+ async def on_server_error(self, server: Any, error: Exception) -> None:
125
+ """Called when a server errors."""
126
+ pass
127
+
128
+ async def on_connect(
129
+ self,
130
+ server: Any,
131
+ additional_params: dict[str, Any],
132
+ ) -> dict[str, Any]:
133
+ """Called before a connection is being established to a mcp server.
134
+
135
+ use `server` (type FlockMCPServer) to modify the core behavior of the server.
136
+ use `additional_params` to 'tack_on' additional configurations (for example additional headers for sse-clients.)
137
+
138
+ (For example: modify the server's config)
139
+ new_config = NewConfigObject(...)
140
+ server.config = new_config
141
+
142
+ Warning:
143
+ Be very careful when modifying a server's internal state.
144
+ If you just need to 'tack on' additional information (such as headers)
145
+ or want to temporarily override certain configurations (such as timeouts)
146
+ use `additional_params` instead if you can.
147
+
148
+ (Or pass additional values downstream:)
149
+ additional_params["headers"] = { "Authorization": "Bearer 123" }
150
+ additional_params["read_timeout_seconds"] = 100
151
+
152
+
153
+ Note:
154
+ `additional_params` resets between mcp_calls.
155
+ so there is not persistence between individual calls.
156
+ This choice has been made to allow developers to
157
+ dynamically switch configurations.
158
+ (This can be used, for example, to use a module to inject oauth headers for
159
+ individual users on a call-to-call basis. this also gives you direct control over
160
+ managing the headers yourself. For example, checking for lifetimes on JWT-Tokens.)
161
+
162
+ Note:
163
+ you can access `additional_params` when you are implementing your own subclasses of
164
+ FlockMCPClientManager and FlockMCPClient. (with self.additional_params.)
165
+
166
+ keys which are processed for `additional_params` in the flock core code are:
167
+ --- General ---
168
+
169
+ "refresh_client": bool -> defaults to False. Indicates whether or not to restart a connection on a call. (can be used when headers oder api-keys change to automatically switch to a new client.)
170
+ "read_timeout_seconds": float -> How long to wait for a connection to happen.
171
+
172
+ --- SSE ---
173
+
174
+ "override_headers": bool -> default False. If set to false, additional headers will be appended, if set to True, additional headers will override existing ones.
175
+ "headers": dict[str, Any] -> Additional Headers injected in sse-clients and ws-clients
176
+ "sse_read_timeout_seconds": float -> how long until a connection is being terminated for sse-clients.
177
+ "url": str -> which url the server listens on (allows switching between mcp-servers with modules.)
178
+
179
+ --- Stdio ---
180
+
181
+ "command": str -> Command to run for stdio-servers.
182
+ "args": list[str] -> additional paramters for stdio-servers.
183
+ "env": dict[str, Any] -> Environment-Variables for stdio-servers.
184
+ "encoding": str -> Encoding to use when talking to stdio-servers.
185
+ "encoding-error-handler": str -> Encoding error handler to use when talking to stdio-servers.
186
+
187
+ --- Websockets ---
188
+
189
+ "url": str -> Which url the server listens on (allows switching between mcp-servers with modules.)
190
+ """
191
+ pass
192
+
193
+ async def on_pre_mcp_call(
194
+ self,
195
+ server: Any,
196
+ arguments: Any | None = None,
197
+ ) -> None:
198
+ """Called before any MCP Calls."""
199
+ pass
200
+
201
+ async def on_post_mcp_call(
202
+ self,
203
+ server: Any,
204
+ result: Any | None = None,
205
+ ) -> None:
206
+ """Called after any MCP Calls."""
207
+ pass
@@ -32,6 +32,7 @@ if TYPE_CHECKING:
32
32
  from flock.core.flock_evaluator import FlockEvaluator
33
33
  from flock.core.flock_module import FlockModule
34
34
  from flock.core.flock_router import FlockRouter
35
+ from flock.core.mcp.flock_mcp_server import FlockMCPServerBase
35
36
 
36
37
  COMPONENT_BASE_TYPES = (FlockModule, FlockEvaluator, FlockRouter)
37
38
 
@@ -43,7 +44,6 @@ else:
43
44
  IS_COMPONENT_CHECK_ENABLED = False
44
45
 
45
46
  # Fallback if core types aren't available during setup
46
-
47
47
  from flock.core.flock_module import FlockModuleConfig
48
48
  from flock.core.logging.logging import get_logger
49
49
 
@@ -56,7 +56,7 @@ _COMPONENT_CONFIG_MAP: dict[type[BaseModel], type[any]] = {}
56
56
 
57
57
 
58
58
  class FlockRegistry:
59
- """Singleton registry for Agents, Callables (functions/methods).
59
+ """Singleton registry for Agents, Callables (functions/methods) and MCP Servers.
60
60
 
61
61
  Types (Pydantic/Dataclasses used in signatures), and Component Classes
62
62
  (Modules, Evaluators, Routers).
@@ -65,6 +65,7 @@ class FlockRegistry:
65
65
  _instance = None
66
66
 
67
67
  _agents: dict[str, FlockAgent]
68
+ _servers: dict[str, FlockMCPServerBase]
68
69
  _callables: dict[str, Callable]
69
70
  _types: dict[str, type]
70
71
  _components: dict[str, type] # For Module, Evaluator, Router classes
@@ -79,6 +80,7 @@ class FlockRegistry:
79
80
  def _initialize(self):
80
81
  """Initialize the internal dictionaries."""
81
82
  self._agents = {}
83
+ self._servers = {}
82
84
  self._callables = {}
83
85
  self._types = {}
84
86
  self._components = {}
@@ -170,6 +172,35 @@ class FlockRegistry:
170
172
  logger.warning(f"Could not determine module/name for object: {obj}")
171
173
  return None
172
174
 
175
+ # --- Server Registration ---
176
+ def register_server(self, server: FlockMCPServerBase) -> None:
177
+ """Registers a flock mcp server by its name."""
178
+ if not hasattr(server.config, "name") or not server.config.name:
179
+ logger.error(
180
+ "Attempted to register a server without a valid 'name' attribute."
181
+ )
182
+ return
183
+ if (
184
+ server.config.name in self._servers
185
+ and self._servers[server.config.name] != server
186
+ ):
187
+ logger.warning(
188
+ f"Server '{server.config.name}' already registered. Overwriting."
189
+ )
190
+ self._servers[server.config.name] = server
191
+ logger.debug(f"Registered server: {server.config.name}")
192
+
193
+ def get_server(self, name: str) -> FlockMCPServerBase | None:
194
+ """Retrieves a registered FlockMCPServer instance by name."""
195
+ server = self._servers.get(name)
196
+ if not server:
197
+ logger.warning(f"Server '{name}' not found in registry.")
198
+ return server
199
+
200
+ def get_all_server_names(self) -> list[str]:
201
+ """Returns a list of names for all registered servers."""
202
+ return list(self._servers.keys())
203
+
173
204
  # --- Agent Registration ---
174
205
  def register_agent(self, agent: FlockAgent, *, force: bool = False) -> None:
175
206
  """Registers a FlockAgent instance by its name.
@@ -361,7 +392,7 @@ class FlockRegistry:
361
392
  else:
362
393
  # Consider adding dynamic import attempts for types if needed,
363
394
  # but explicit registration is generally safer for types.
364
- logger.error(f"Type '{type_name}' not found in registry.")
395
+ logger.warning(f"Type '{type_name}' not found in registry. Will attempt to build it from builtins.")
365
396
  raise KeyError(
366
397
  f"Type '{type_name}' not found. Ensure it is registered."
367
398
  )
@@ -499,6 +530,8 @@ def get_registry() -> FlockRegistry:
499
530
  # Type hinting for decorators to preserve signature
500
531
  @overload
501
532
  def flock_component(cls: ClassType) -> ClassType: ... # Basic registration
533
+
534
+
502
535
  @overload
503
536
  def flock_component(
504
537
  *, name: str | None = None, config_class: type[ConfigType] | None = None
@@ -542,6 +575,8 @@ def flock_component(
542
575
  # Type hinting for decorators
543
576
  @overload
544
577
  def flock_tool(func: FuncType) -> FuncType: ...
578
+
579
+
545
580
  @overload
546
581
  def flock_tool(
547
582
  *, name: str | None = None
@@ -581,6 +616,8 @@ flock_callable = flock_tool
581
616
 
582
617
  @overload
583
618
  def flock_type(cls: ClassType) -> ClassType: ...
619
+
620
+
584
621
  @overload
585
622
  def flock_type(
586
623
  *, name: str | None = None
@@ -0,0 +1,136 @@
1
+ """Manages Server-Lifecycles within the larger lifecycle of Flock."""
2
+
3
+ import asyncio
4
+ from contextlib import AsyncExitStack
5
+
6
+ from anyio import Lock
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+ from flock.core.mcp.flock_mcp_server import FlockMCPServerBase
10
+
11
+
12
+ class FlockServerManager(BaseModel):
13
+ """Async-context-manager to start/stop a set of Flock MCP servers."""
14
+
15
+ servers: list[FlockMCPServerBase] | None = Field(
16
+ ..., exclude=True, description="The servers to manage."
17
+ )
18
+
19
+ stack: AsyncExitStack | None = Field(
20
+ default=None,
21
+ exclude=True,
22
+ description="Central exit stack for managing the execution context of the servers.",
23
+ )
24
+
25
+ lock: Lock | None = Field(
26
+ default=None, exclude=True, description="Global lock for mutex access."
27
+ )
28
+
29
+ model_config = ConfigDict(
30
+ arbitrary_types_allowed=True,
31
+ )
32
+
33
+ def __init__(
34
+ self,
35
+ servers: list[FlockMCPServerBase] | None = None,
36
+ stack: AsyncExitStack | None = None,
37
+ lock: asyncio.Lock | None = None,
38
+ ) -> None:
39
+ """Initialize the FlockServerManager with optional server, stack, and lock references."""
40
+ super().__init__(
41
+ servers=servers,
42
+ stack=stack,
43
+ lock=lock,
44
+ )
45
+
46
+ def add_server_sync(self, server: FlockMCPServerBase) -> None:
47
+ """Add a server to be managed by the ServerManager.
48
+
49
+ Note:
50
+ IT IS CRUCIAL THAT THIS METHOD IS NOT CALLED
51
+ WHEN THE SERVER MANAGER HAS ALREADY BEEN INTIALIZED
52
+ (with server_manager as manager: ...)
53
+ OTHERWISE EXECUTION WILL BREAK DOWN.
54
+ """
55
+ if self.servers is None:
56
+ self.servers = []
57
+
58
+ self.servers.append(server)
59
+
60
+ def remove_server_sync(self, server: FlockMCPServerBase) -> None:
61
+ """Remove a server from the list of managed servers.
62
+
63
+ Note:
64
+ IT IS CRUCIAL THAT THIS METHOD IS NOT CALLED
65
+ WHEN THE SERVER MANAGER HAS ALREADY BEEN INITIALIZED
66
+ (with server_manager as manager: ...)
67
+ OTHERWISE EXECUTION WILL BREAK DOWN.
68
+ """
69
+ if self.servers and server in self.servers:
70
+ self.servers.remove(server)
71
+
72
+ # -- For future use: Allow adding and removal of servers during runtime ---
73
+ async def add_server_during_runtime(
74
+ self, server: FlockMCPServerBase
75
+ ) -> None:
76
+ """Add a server to the manager and, if already running, start it immediately."""
77
+ if self.lock is None:
78
+ self.lock = asyncio.Lock()
79
+
80
+ async with self.lock:
81
+ if self.servers is None:
82
+ self.servers = []
83
+
84
+ self.servers.append(server)
85
+
86
+ # If we are already running in async-with, enter the context now
87
+ if self.stack is not None:
88
+ await self.stack.enter_async_context(server)
89
+
90
+ async def remove_server_during_runtime(
91
+ self, server: FlockMCPServerBase
92
+ ) -> None:
93
+ """Tear down and remove a server from the manager at runtime."""
94
+ if self.lock is None:
95
+ self.lock = asyncio.Lock()
96
+
97
+ retrieved_server: FlockMCPServerBase | None = None
98
+
99
+ async with self.lock:
100
+ if not self.servers or server not in self.servers:
101
+ return # Skip as to not impede application flow
102
+ else:
103
+ try:
104
+ self.servers.remove(server)
105
+ retrieved_server = server
106
+ except ValueError:
107
+ # The server is not present (a little paranoid at this point, but still...)
108
+ return
109
+
110
+ # tell the server to shut down.
111
+ if retrieved_server:
112
+ # trigger the server's own exit hook (this closes its connection_manager, sessions, tools....)
113
+ await retrieved_server.__aexit__(None, None, None)
114
+
115
+ async def __aenter__(self) -> "FlockServerManager":
116
+ """Enter the asynchronous context for the server manager."""
117
+ if not self.stack:
118
+ self.stack = AsyncExitStack()
119
+
120
+ if not self.servers:
121
+ self.servers = []
122
+
123
+ if not self.lock:
124
+ self.lock = asyncio.Lock()
125
+
126
+ for srv in self.servers:
127
+ await self.stack.enter_async_context(srv)
128
+
129
+ return self
130
+
131
+ async def __aexit__(self, exc_type, exc, tb) -> None:
132
+ """Exit the asynchronous context for the server manager."""
133
+ # Unwind the servers in LIFO order
134
+ if self.stack is not None:
135
+ await self.stack.aclose()
136
+ self.stack = None
@@ -1,2 +1,6 @@
1
1
  """Flock logging system with Rich integration and structured logging support."""
2
2
 
3
+ from .logging import configure_logging
4
+
5
+ __all__ = ["configure_logging"]
6
+