flock-core 0.4.512__py3-none-any.whl → 0.4.513__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/flock_agent.py CHANGED
@@ -384,7 +384,7 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
384
384
  )
385
385
  else:
386
386
  logger.warning(
387
- f"No Server with name '{server}' registered! Skipping."
387
+ f"No Server with name '{server.config.name}' registered! Skipping."
388
388
  )
389
389
  mcp_tools = mcp_tools + server_tools
390
390
 
@@ -5,7 +5,8 @@ from collections.abc import Callable
5
5
  from pathlib import Path
6
6
  from typing import Any, Literal
7
7
 
8
- from pydantic import AnyUrl, BaseModel, Field, FileUrl
8
+ import httpx
9
+ from pydantic import AnyUrl, BaseModel, ConfigDict, Field, FileUrl
9
10
 
10
11
  from flock.core.config.scheduled_agent_config import ScheduledAgentConfig
11
12
  from flock.core.flock_agent import FlockAgent, SignatureType
@@ -24,6 +25,7 @@ from flock.core.mcp.types.types import (
24
25
  MCPRoot,
25
26
  SseServerParameters,
26
27
  StdioServerParameters,
28
+ StreamableHttpServerParameters,
27
29
  WebsocketServerParameters,
28
30
  )
29
31
  from flock.evaluators.declarative.declarative_evaluator import (
@@ -40,6 +42,11 @@ from flock.mcp.servers.stdio.flock_stdio_server import (
40
42
  FlockStdioConfig,
41
43
  FlockStdioConnectionConfig,
42
44
  )
45
+ from flock.mcp.servers.streamable_http.flock_streamable_http_server import (
46
+ FlockStreamableHttpConfig,
47
+ FlockStreamableHttpConnectionConfig,
48
+ FlockStreamableHttpServer,
49
+ )
43
50
  from flock.mcp.servers.websockets.flock_websocket_server import (
44
51
  FlockWSConfig,
45
52
  FlockWSConnectionConfig,
@@ -101,6 +108,44 @@ class FlockFactory:
101
108
  description="The text encoding error handler. See https://docs.python.org/3/library/codecs.html#codec-base-classes for explanations of possible values",
102
109
  )
103
110
 
111
+ class StreamableHttpParams(BaseModel):
112
+ """Factory-Params for Streamable Http Servers."""
113
+
114
+ url: str | AnyUrl = Field(
115
+ ...,
116
+ description="Url the server listens at."
117
+ )
118
+
119
+ headers: dict[str, Any] | None = Field(
120
+ default=None,
121
+ description="Additional Headers to pass to the client."
122
+ )
123
+
124
+ auth: httpx.Auth | None = Field(
125
+ default=None,
126
+ description="Httpx Auth Schema."
127
+ )
128
+
129
+ timeout_seconds: float | int = Field(
130
+ default=5,
131
+ description="Http Timeout in Seconds"
132
+ )
133
+
134
+ sse_read_timeout_seconds: float | int = Field(
135
+ default=60*5,
136
+ description="How many seconds to wait for server-sent events until closing the connection."
137
+ )
138
+
139
+ terminate_on_close: bool = Field(
140
+ default=True,
141
+ description="Whether or not to terminate the underlying connection on close."
142
+ )
143
+
144
+ model_config = ConfigDict(
145
+ arbitrary_types_allowed=True,
146
+ extra="allow",
147
+ )
148
+
104
149
  class SSEParams(BaseModel):
105
150
  """Factory-Params for SSE-Servers."""
106
151
 
@@ -123,6 +168,16 @@ class FlockFactory:
123
168
  description="How many seconds to wait for server-sent events until closing the connection. (connections will be automatically re-established.)",
124
169
  )
125
170
 
171
+ auth: httpx.Auth | None = Field(
172
+ default=None,
173
+ description="Httpx Auth Scheme."
174
+ )
175
+
176
+ model_config = ConfigDict(
177
+ arbitrary_types_allowed=True,
178
+ extra="allow",
179
+ )
180
+
126
181
  class WebsocketParams(BaseModel):
127
182
  """Factory-Params for Websocket Servers."""
128
183
 
@@ -134,7 +189,7 @@ class FlockFactory:
134
189
  @staticmethod
135
190
  def create_mcp_server(
136
191
  name: str,
137
- connection_params: SSEParams | StdioParams | WebsocketParams,
192
+ connection_params: StreamableHttpParams | SSEParams | StdioParams | WebsocketParams,
138
193
  max_retries: int = 3,
139
194
  mount_points: list[str | MCPRoot] | None = None,
140
195
  timeout_seconds: int | float = 10,
@@ -176,6 +231,9 @@ class FlockFactory:
176
231
  if isinstance(connection_params, FlockFactory.WebsocketParams):
177
232
  server_kind = "websockets"
178
233
  concrete_server_cls = FlockWSServer
234
+ if isinstance(connection_params, FlockFactory.StreamableHttpParams):
235
+ server_kind = "streamable_http"
236
+ concrete_server_cls = FlockStreamableHttpServer
179
237
 
180
238
  # convert mount points.
181
239
  mounts: list[MCPRoot] = []
@@ -244,12 +302,37 @@ class FlockFactory:
244
302
  caching_config=caching_config,
245
303
  callback_config=callback_config,
246
304
  )
305
+ elif server_kind == "streamable_http":
306
+ # build streamable http config
307
+ connection_config = FlockStreamableHttpConnectionConfig(
308
+ max_retries=max_retries,
309
+ connection_parameters=StreamableHttpServerParameters(
310
+ url=connection_params.url,
311
+ headers=connection_params.headers,
312
+ auth=connection_params.auth,
313
+ timeout=connection_params.timeout_seconds,
314
+ sse_read_timeout=connection_params.sse_read_timeout_seconds,
315
+ terminate_on_close=connection_params.terminate_on_close,
316
+ ),
317
+ mount_points=mounts,
318
+ server_logging_level=server_logging_level,
319
+ )
320
+
321
+ server_config = FlockStreamableHttpConfig(
322
+ name=name,
323
+ connection_config=connection_config,
324
+ feature_config=feature_config,
325
+ caching_config=caching_config,
326
+ callback_config=callback_config,
327
+ )
328
+
247
329
  elif server_kind == "sse":
248
330
  # build sse config
249
331
  connection_config = FlockSSEConnectionConfig(
250
332
  max_retries=max_retries,
251
333
  connection_parameters=SseServerParameters(
252
334
  url=connection_params.url,
335
+ auth=connection_params.auth,
253
336
  headers=connection_params.headers,
254
337
  timeout=connection_params.timeout_seconds,
255
338
  sse_read_timeout=connection_params.sse_read_timeout_seconds,
@@ -68,6 +68,14 @@ COLOR_MAP = {
68
68
  "workflow": "cyan", # Color only
69
69
  "activities": "cyan",
70
70
  "context": "green",
71
+ "mcp.server": "blue",
72
+ "mcp.tool": "cyan",
73
+ "mcp.client_manager": "light-blue",
74
+ "mcp.client": "light-cyan",
75
+ "mcp.callback.logging": "white",
76
+ "mcp.callback.sampling": "pink",
77
+ "mcp.callback.root": "light-yellow",
78
+ "mcp.callback.message": "light-blue",
71
79
  # Components & Mechanisms
72
80
  "registry": "yellow", # Color only
73
81
  "serialization": "yellow",
@@ -26,7 +26,7 @@ from flock.core.serialization.serialization_utils import (
26
26
  serialize_item,
27
27
  )
28
28
 
29
- logger = get_logger("core.mcp.server_base")
29
+ logger = get_logger("mcp.server")
30
30
  tracer = trace.get_tracer(__name__)
31
31
  T = TypeVar("T", bound="FlockMCPServerBase")
32
32
 
@@ -206,7 +206,6 @@ class FlockMCPServerBase(BaseModel, Serializable, ABC):
206
206
  async with self.condition:
207
207
  try:
208
208
  await self.pre_mcp_call()
209
- # TODO: inject additional params here.
210
209
  additional_params: dict[str, Any] = {}
211
210
  additional_params = await self.before_connect(
212
211
  additional_params=additional_params
@@ -314,7 +313,7 @@ class FlockMCPServerBase(BaseModel, Serializable, ABC):
314
313
  async def post_terminate(self) -> None:
315
314
  """Run post-terminate hooks on modules."""
316
315
  logger.debug(
317
- f"Running post_terminat hooks for modules in server: '{self.config.name}'"
316
+ f"Running post_terminate hooks for modules in server: '{self.config.name}'"
318
317
  )
319
318
  with tracer.start_as_current_span("server.post_terminate") as span:
320
319
  span.set_attribute("server.name", self.config.name)
@@ -437,7 +436,7 @@ class FlockMCPServerBase(BaseModel, Serializable, ABC):
437
436
 
438
437
  FlockRegistry = get_registry()
439
438
 
440
- exclude = ["modules"]
439
+ exclude = ["modules", "config"]
441
440
 
442
441
  logger.debug(f"Serializing server '{self.config.name}' to dict.")
443
442
  # Use Pydantic's dump, exclued manually handled fields.
@@ -447,6 +446,11 @@ class FlockMCPServerBase(BaseModel, Serializable, ABC):
447
446
  exclude_none=True, # Exclude None values for cleaner output
448
447
  )
449
448
 
449
+ # --- Let the config handle its own serialization ---
450
+ config_data = self.config.to_dict(path_type=path_type)
451
+ data["config"] = config_data
452
+
453
+
450
454
  builtin_by_transport = {}
451
455
 
452
456
  try:
@@ -454,12 +458,16 @@ class FlockMCPServerBase(BaseModel, Serializable, ABC):
454
458
  from flock.mcp.servers.stdio.flock_stdio_server import (
455
459
  FlockMCPStdioServer,
456
460
  )
461
+ from flock.mcp.servers.streamable_http.flock_streamable_http_server import (
462
+ FlockStreamableHttpServer,
463
+ )
457
464
  from flock.mcp.servers.websockets.flock_websocket_server import (
458
465
  FlockWSServer,
459
466
  )
460
467
 
461
468
  builtin_by_transport = {
462
469
  "stdio": FlockMCPStdioServer,
470
+ "streamable_http": FlockStreamableHttpServer,
463
471
  "sse": FlockSSEServer,
464
472
  "websockets": FlockWSServer,
465
473
  }
@@ -570,6 +578,9 @@ class FlockMCPServerBase(BaseModel, Serializable, ABC):
570
578
  from flock.mcp.servers.stdio.flock_stdio_server import (
571
579
  FlockMCPStdioServer,
572
580
  )
581
+ from flock.mcp.servers.streamable_http.flock_streamable_http_server import (
582
+ FlockStreamableHttpServer,
583
+ )
573
584
  from flock.mcp.servers.websockets.flock_websocket_server import (
574
585
  FlockWSServer,
575
586
  )
@@ -577,6 +588,7 @@ class FlockMCPServerBase(BaseModel, Serializable, ABC):
577
588
  builtin_by_transport = {
578
589
  "stdio": FlockMCPStdioServer,
579
590
  "sse": FlockSSEServer,
591
+ "streamable_http": FlockStreamableHttpServer,
580
592
  "websockets": FlockWSServer,
581
593
  }
582
594
  except ImportError:
@@ -592,6 +604,20 @@ class FlockMCPServerBase(BaseModel, Serializable, ABC):
592
604
  transport = data["config"]["connection_config"]["transport_type"]
593
605
  real_cls = builtin_by_transport.get(transport, cls)
594
606
 
607
+ # deserialize the config:
608
+ config_data = data.pop("config", None)
609
+ if config_data:
610
+ # Forcing a square into a round hole
611
+ # pretty ugly, but gets the job done.
612
+ try:
613
+ config_field = real_cls.model_fields["config"]
614
+ config_cls = config_field.annotation
615
+ except (AttributeError, KeyError):
616
+ # fallback if Pydantic v1 or missing
617
+ config_cls = FlockMCPConfigurationBase
618
+ config_object = config_cls.from_dict(config_data)
619
+ data["config"] = config_object
620
+
595
621
  # now construct
596
622
  server = real_cls(**{k: v for k, v in data.items() if k != "modules"})
597
623
 
@@ -10,7 +10,7 @@ from pydantic import BaseModel, Field
10
10
 
11
11
  from flock.core.logging.logging import get_logger
12
12
 
13
- logger = get_logger("core.mcp.tool_base")
13
+ logger = get_logger("mcp.tool")
14
14
  tracer = trace.get_tracer(__name__)
15
15
 
16
16
  T = TypeVar("T", bound="FlockMCPToolBase")
@@ -4,9 +4,10 @@ import asyncio
4
4
  import random
5
5
  from abc import ABC, abstractmethod
6
6
  from asyncio import Lock
7
+ from collections.abc import Callable
7
8
  from contextlib import (
8
- AbstractAsyncContextManager,
9
9
  AsyncExitStack,
10
+ asynccontextmanager,
10
11
  )
11
12
  from datetime import timedelta
12
13
  from typing import (
@@ -27,7 +28,7 @@ from mcp import (
27
28
  McpError,
28
29
  ServerCapabilities,
29
30
  )
30
- from mcp.types import CallToolResult, JSONRPCMessage
31
+ from mcp.types import CallToolResult
31
32
  from opentelemetry import trace
32
33
  from pydantic import (
33
34
  BaseModel,
@@ -54,9 +55,11 @@ from flock.core.mcp.types.types import (
54
55
  )
55
56
  from flock.core.mcp.util.helpers import cache_key_generator
56
57
 
57
- logger = get_logger("core.mcp.client_base")
58
+ logger = get_logger("mcp.client")
58
59
  tracer = trace.get_tracer(__name__)
59
60
 
61
+ GetSessionIdCallback = Callable[[], str | None]
62
+
60
63
 
61
64
  class FlockMCPClientBase(BaseModel, ABC):
62
65
  """Wrapper for mcp ClientSession.
@@ -159,11 +162,9 @@ class FlockMCPClientBase(BaseModel, ABC):
159
162
  max_tries = cfg.connection_config.max_retries or 1
160
163
  base_delay = 0.1
161
164
  span.set_attribute("client.name", client.config.name)
165
+ span.set_attribute("max_tries", max_tries)
162
166
 
163
167
  for attempt in range(1, max_tries + 2):
164
- span.set_attribute(
165
- "max_tries", max_tries
166
- ) # TODO: shift outside of loop
167
168
  span.set_attribute("base_delay", base_delay)
168
169
  span.set_attribute("attempt", attempt)
169
170
  await client._ensure_connected()
@@ -364,12 +365,7 @@ class FlockMCPClientBase(BaseModel, ABC):
364
365
  self,
365
366
  params: ServerParameters,
366
367
  additional_params: dict[str, Any] | None = None,
367
- ) -> AbstractAsyncContextManager[
368
- tuple[
369
- MemoryObjectReceiveStream[JSONRPCMessage | Exception],
370
- MemoryObjectSendStream[JSONRPCMessage],
371
- ]
372
- ]:
368
+ ) -> Any:
373
369
  """Given your custom ServerParameters, return an async-contextmgr whose __aenter yields (read_stream, write_stream)."""
374
370
  ...
375
371
 
@@ -390,6 +386,7 @@ class FlockMCPClientBase(BaseModel, ABC):
390
386
  return []
391
387
 
392
388
  async def _get_tools_internal() -> list[FlockMCPToolBase]:
389
+ # TODO: Crash
393
390
  response: ListToolsResult = await self.session.list_tools()
394
391
  flock_tools = []
395
392
 
@@ -520,13 +517,31 @@ class FlockMCPClientBase(BaseModel, ABC):
520
517
  if self.session_stack:
521
518
  # manually __aexit__
522
519
  await self.session_stack.aclose()
523
- self.session_stack = None
524
- self.client_session = None
520
+ self.client_session = None # remove the reference
525
521
 
526
522
  # --- Private Methods ---
523
+ @asynccontextmanager
524
+ async def _safe_transport_ctx(self, cm: Any):
525
+ """Enter the real transport ctxmg, yield its value, but on __aexit__ always swallow all errors."""
526
+ val = await cm.__aenter__()
527
+ try:
528
+ yield val
529
+ finally:
530
+ try:
531
+ await cm.__aexit__(None, None, None)
532
+ except Exception as e:
533
+ logger.debug(
534
+ f"Suppressed transport-ctx exit error "
535
+ f"for server '{self.config.name}': {e!r}"
536
+ )
537
+
527
538
  async def _create_session(self) -> None:
528
- """Create and hol onto a single ClientSession + ExitStack."""
539
+ """Create and hold onto a single ClientSession + ExitStack."""
529
540
  logger.debug(f"Creating Client Session for server '{self.config.name}'")
541
+ if self.session_stack:
542
+ await self.session_stack.aclose()
543
+ if self.client_session:
544
+ self.client_session = None
530
545
  stack = AsyncExitStack()
531
546
  await stack.__aenter__()
532
547
 
@@ -536,7 +551,30 @@ class FlockMCPClientBase(BaseModel, ABC):
536
551
  transport_ctx = await self.create_transport(
537
552
  server_params, self.additional_params
538
553
  )
539
- read, write = await stack.enter_async_context(transport_ctx)
554
+ safe_transport = self._safe_transport_ctx(transport_ctx)
555
+ result = await stack.enter_async_context(safe_transport)
556
+
557
+ # support old (read, write) or new (read, write, get_sesssion_id_callback)
558
+ read: MemoryObjectReceiveStream | None = None
559
+ write: MemoryObjectSendStream | None = None
560
+ get_session_id_callback: GetSessionIdCallback | None = None
561
+ if isinstance(result, tuple) and len(result) == 2:
562
+ # old type
563
+ read, write = result
564
+ get_session_id_callback = None
565
+ elif isinstance(result, tuple) and len(result) == 3:
566
+ # new type
567
+ read, write, get_session_id_callback = result
568
+ else:
569
+ raise RuntimeError(
570
+ f"create_transport returned unexpected tuple of {result}"
571
+ )
572
+
573
+ if read is None or write is None:
574
+ raise RuntimeError(
575
+ f"create_transport did not create any read or write streams."
576
+ )
577
+
540
578
  read_timeout = self.config.connection_config.read_timeout_seconds
541
579
 
542
580
  if (
@@ -553,6 +591,8 @@ class FlockMCPClientBase(BaseModel, ABC):
553
591
  else timedelta(seconds=float(read_timeout))
554
592
  )
555
593
 
594
+ # TODO: get_session_id_callback is currently ignored.
595
+
556
596
  session = await stack.enter_async_context(
557
597
  ClientSession(
558
598
  read_stream=read,
@@ -603,18 +643,7 @@ class FlockMCPClientBase(BaseModel, ABC):
603
643
 
604
644
  self.connected_server_capabilities = init
605
645
 
606
- init_report = f"""
607
- Server Init Handshake completed Server '{self.config.name}'
608
- Lists the following Capabilities:
609
-
610
- - Protocol Version: {init.protocolVersion}
611
- - Instructions: {init.instructions or "No specific Instructions"}
612
- - MCP Implementation:
613
- - Name: {init.serverInfo.name}
614
- - Version: {init.serverInfo.version}
615
- - Capabilities:
616
- {init.capabilities}
617
- """
646
+ init_report = f"Server: '{self.config.name}': Protocol-Version: {init.protocolVersion}, Instructions: {init.instructions or 'No specific instructions'}, MCP_Implementation: Name: {init.serverInfo.name}, Version: {init.serverInfo.version}, Capabilities: {init.capabilities}"
618
647
 
619
648
  logger.debug(init_report)
620
649
 
@@ -19,7 +19,7 @@ from flock.core.mcp.mcp_client import (
19
19
  )
20
20
  from flock.core.mcp.mcp_config import FlockMCPConfigurationBase
21
21
 
22
- logger = get_logger("core.mcp.connection_manager_base")
22
+ logger = get_logger("mcp.client_manager")
23
23
  tracer = trace.get_tracer(__name__)
24
24
 
25
25
  TClient = TypeVar("TClient", bound="FlockMCPClientBase")