flock-core 0.4.3__py3-none-any.whl → 0.4.5__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.
- flock/core/__init__.py +11 -0
- flock/core/flock.py +144 -42
- flock/core/flock_agent.py +117 -4
- flock/core/flock_evaluator.py +1 -1
- flock/core/flock_factory.py +290 -2
- flock/core/flock_module.py +101 -0
- flock/core/flock_registry.py +39 -2
- flock/core/flock_server_manager.py +136 -0
- flock/core/logging/telemetry.py +1 -1
- flock/core/mcp/__init__.py +1 -0
- flock/core/mcp/flock_mcp_server.py +614 -0
- flock/core/mcp/flock_mcp_tool_base.py +201 -0
- flock/core/mcp/mcp_client.py +658 -0
- flock/core/mcp/mcp_client_manager.py +201 -0
- flock/core/mcp/mcp_config.py +237 -0
- flock/core/mcp/types/__init__.py +1 -0
- flock/core/mcp/types/callbacks.py +86 -0
- flock/core/mcp/types/factories.py +111 -0
- flock/core/mcp/types/handlers.py +240 -0
- flock/core/mcp/types/types.py +157 -0
- flock/core/mcp/util/__init__.py +0 -0
- flock/core/mcp/util/helpers.py +23 -0
- flock/core/mixin/dspy_integration.py +45 -12
- flock/core/serialization/flock_serializer.py +52 -1
- flock/core/util/spliter.py +4 -0
- flock/evaluators/declarative/declarative_evaluator.py +4 -3
- flock/mcp/servers/sse/__init__.py +1 -0
- flock/mcp/servers/sse/flock_sse_server.py +139 -0
- flock/mcp/servers/stdio/__init__.py +1 -0
- flock/mcp/servers/stdio/flock_stdio_server.py +138 -0
- flock/mcp/servers/websockets/__init__.py +1 -0
- flock/mcp/servers/websockets/flock_websocket_server.py +119 -0
- flock/modules/performance/metrics_module.py +159 -1
- {flock_core-0.4.3.dist-info → flock_core-0.4.5.dist-info}/METADATA +4 -2
- {flock_core-0.4.3.dist-info → flock_core-0.4.5.dist-info}/RECORD +38 -18
- {flock_core-0.4.3.dist-info → flock_core-0.4.5.dist-info}/WHEEL +0 -0
- {flock_core-0.4.3.dist-info → flock_core-0.4.5.dist-info}/entry_points.txt +0 -0
- {flock_core-0.4.3.dist-info → flock_core-0.4.5.dist-info}/licenses/LICENSE +0 -0
flock/core/flock_factory.py
CHANGED
|
@@ -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
|
|
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,
|
flock/core/flock_module.py
CHANGED
|
@@ -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
|
flock/core/flock_registry.py
CHANGED
|
@@ -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.
|
|
@@ -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
|
flock/core/logging/telemetry.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Flock MCP package."""
|