flock-core 0.4.511__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.

Files changed (56) hide show
  1. flock/core/config/flock_agent_config.py +11 -0
  2. flock/core/config/scheduled_agent_config.py +40 -0
  3. flock/core/flock_agent.py +7 -1
  4. flock/core/flock_factory.py +129 -2
  5. flock/core/flock_scheduler.py +166 -0
  6. flock/core/logging/logging.py +8 -0
  7. flock/core/mcp/flock_mcp_server.py +30 -4
  8. flock/core/mcp/flock_mcp_tool_base.py +1 -1
  9. flock/core/mcp/mcp_client.py +57 -28
  10. flock/core/mcp/mcp_client_manager.py +1 -1
  11. flock/core/mcp/mcp_config.py +245 -9
  12. flock/core/mcp/types/callbacks.py +3 -5
  13. flock/core/mcp/types/factories.py +12 -14
  14. flock/core/mcp/types/handlers.py +9 -12
  15. flock/core/mcp/types/types.py +205 -2
  16. flock/mcp/servers/sse/flock_sse_server.py +21 -14
  17. flock/mcp/servers/streamable_http/__init__.py +0 -0
  18. flock/mcp/servers/streamable_http/flock_streamable_http_server.py +169 -0
  19. flock/mcp/servers/websockets/flock_websocket_server.py +3 -3
  20. flock/webapp/app/main.py +66 -11
  21. flock/webapp/app/services/sharing_store.py +173 -0
  22. flock/webapp/run.py +3 -1
  23. flock/webapp/templates/base.html +10 -11
  24. flock/webapp/templates/chat.html +7 -10
  25. flock/webapp/templates/chat_settings.html +3 -4
  26. flock/webapp/templates/flock_editor.html +1 -2
  27. flock/webapp/templates/index.html +1 -1
  28. flock/webapp/templates/partials/_agent_detail_form.html +7 -13
  29. flock/webapp/templates/partials/_agent_list.html +1 -2
  30. flock/webapp/templates/partials/_agent_manager_view.html +2 -3
  31. flock/webapp/templates/partials/_chat_container.html +2 -2
  32. flock/webapp/templates/partials/_chat_settings_form.html +6 -8
  33. flock/webapp/templates/partials/_create_flock_form.html +2 -4
  34. flock/webapp/templates/partials/_dashboard_flock_detail.html +2 -3
  35. flock/webapp/templates/partials/_dashboard_flock_file_list.html +1 -2
  36. flock/webapp/templates/partials/_dashboard_flock_properties_preview.html +2 -3
  37. flock/webapp/templates/partials/_dashboard_upload_flock_form.html +1 -2
  38. flock/webapp/templates/partials/_env_vars_table.html +2 -4
  39. flock/webapp/templates/partials/_execution_form.html +12 -10
  40. flock/webapp/templates/partials/_execution_view_container.html +2 -3
  41. flock/webapp/templates/partials/_flock_file_list.html +2 -3
  42. flock/webapp/templates/partials/_flock_properties_form.html +2 -2
  43. flock/webapp/templates/partials/_flock_upload_form.html +1 -2
  44. flock/webapp/templates/partials/_load_manager_view.html +2 -3
  45. flock/webapp/templates/partials/_registry_viewer_content.html +4 -5
  46. flock/webapp/templates/partials/_settings_env_content.html +2 -3
  47. flock/webapp/templates/partials/_settings_theme_content.html +2 -2
  48. flock/webapp/templates/partials/_settings_view.html +2 -2
  49. flock/webapp/templates/partials/_sidebar.html +27 -39
  50. flock/webapp/templates/registry_viewer.html +7 -10
  51. flock/webapp/templates/shared_run_page.html +7 -10
  52. {flock_core-0.4.511.dist-info → flock_core-0.4.513.dist-info}/METADATA +3 -1
  53. {flock_core-0.4.511.dist-info → flock_core-0.4.513.dist-info}/RECORD +56 -51
  54. {flock_core-0.4.511.dist-info → flock_core-0.4.513.dist-info}/WHEEL +0 -0
  55. {flock_core-0.4.511.dist-info → flock_core-0.4.513.dist-info}/entry_points.txt +0 -0
  56. {flock_core-0.4.511.dist-info → flock_core-0.4.513.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,11 @@
1
+
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class FlockAgentConfig(BaseModel):
7
+ """FlockAgentConfig is a class that holds the configuration for a Flock agent.
8
+ It is used to store various settings and parameters that can be accessed throughout the agent's lifecycle.
9
+ """
10
+
11
+ pass
@@ -0,0 +1,40 @@
1
+
2
+
3
+
4
+ from pydantic import Field
5
+
6
+ from flock.core.config.flock_agent_config import FlockAgentConfig
7
+
8
+
9
+ class ScheduledAgentConfig(FlockAgentConfig):
10
+ """Configuration specific to agents that run on a schedule."""
11
+ schedule_expression: str = Field(
12
+ ...,
13
+ description="Defines when the agent should run. "
14
+ "Examples: 'every 60m', 'every 1h', 'daily at 02:00', '0 */2 * * *' (cron expression)"
15
+ )
16
+ enabled: bool = Field(
17
+ True,
18
+ description="Whether the scheduled agent is enabled. "
19
+ "If False, the agent will not run even if the schedule expression is valid."
20
+ )
21
+ initial_run: bool = Field(
22
+ False,
23
+ description="If True, the agent will run immediately after being scheduled, "
24
+ "regardless of the schedule expression."
25
+ )
26
+ max_runs: int = Field(
27
+ 0,
28
+ description="Maximum number of times the agent can run. "
29
+ "0 means unlimited runs. If set, the agent will stop running after reaching this limit."
30
+ )
31
+
32
+ def __init__(self, **kwargs):
33
+ super().__init__(**kwargs)
34
+ # Ensure schedule_expression is always set
35
+ if 'schedule_expression' not in kwargs:
36
+ raise ValueError("schedule_expression is required for ScheduledAgentConfig")
37
+
38
+ # Validate initial_run and max_runs
39
+ if self.initial_run and self.max_runs > 0:
40
+ raise ValueError("Cannot set initial_run to True if max_runs is greater than 0")
flock/core/flock_agent.py CHANGED
@@ -10,6 +10,7 @@ from collections.abc import Callable
10
10
  from datetime import datetime
11
11
  from typing import TYPE_CHECKING, Any, TypeVar
12
12
 
13
+ from flock.core.config.flock_agent_config import FlockAgentConfig
13
14
  from flock.core.mcp.flock_mcp_server import FlockMCPServerBase
14
15
  from flock.core.serialization.json_encoder import FlockJSONEncoder
15
16
  from flock.workflow.temporal_config import TemporalActivityConfig
@@ -124,6 +125,11 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
124
125
  description="Dictionary of FlockModules attached to this agent.",
125
126
  )
126
127
 
128
+ config: FlockAgentConfig = Field(
129
+ default_factory=lambda: FlockAgentConfig(),
130
+ description="Configuration for this agent, holding various settings and parameters.",
131
+ )
132
+
127
133
  # --- Temporal Configuration (Optional) ---
128
134
  temporal_activity_config: TemporalActivityConfig | None = Field(
129
135
  default=None,
@@ -378,7 +384,7 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
378
384
  )
379
385
  else:
380
386
  logger.warning(
381
- f"No Server with name '{server}' registered! Skipping."
387
+ f"No Server with name '{server.config.name}' registered! Skipping."
382
388
  )
383
389
  mcp_tools = mcp_tools + server_tools
384
390
 
@@ -5,8 +5,10 @@ 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
 
11
+ from flock.core.config.scheduled_agent_config import ScheduledAgentConfig
10
12
  from flock.core.flock_agent import FlockAgent, SignatureType
11
13
  from flock.core.logging.formatters.themes import OutputTheme
12
14
  from flock.core.mcp.flock_mcp_server import FlockMCPServerBase
@@ -23,6 +25,7 @@ from flock.core.mcp.types.types import (
23
25
  MCPRoot,
24
26
  SseServerParameters,
25
27
  StdioServerParameters,
28
+ StreamableHttpServerParameters,
26
29
  WebsocketServerParameters,
27
30
  )
28
31
  from flock.evaluators.declarative.declarative_evaluator import (
@@ -39,6 +42,11 @@ from flock.mcp.servers.stdio.flock_stdio_server import (
39
42
  FlockStdioConfig,
40
43
  FlockStdioConnectionConfig,
41
44
  )
45
+ from flock.mcp.servers.streamable_http.flock_streamable_http_server import (
46
+ FlockStreamableHttpConfig,
47
+ FlockStreamableHttpConnectionConfig,
48
+ FlockStreamableHttpServer,
49
+ )
42
50
  from flock.mcp.servers.websockets.flock_websocket_server import (
43
51
  FlockWSConfig,
44
52
  FlockWSConnectionConfig,
@@ -100,6 +108,44 @@ class FlockFactory:
100
108
  description="The text encoding error handler. See https://docs.python.org/3/library/codecs.html#codec-base-classes for explanations of possible values",
101
109
  )
102
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
+
103
149
  class SSEParams(BaseModel):
104
150
  """Factory-Params for SSE-Servers."""
105
151
 
@@ -122,6 +168,16 @@ class FlockFactory:
122
168
  description="How many seconds to wait for server-sent events until closing the connection. (connections will be automatically re-established.)",
123
169
  )
124
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
+
125
181
  class WebsocketParams(BaseModel):
126
182
  """Factory-Params for Websocket Servers."""
127
183
 
@@ -133,7 +189,7 @@ class FlockFactory:
133
189
  @staticmethod
134
190
  def create_mcp_server(
135
191
  name: str,
136
- connection_params: SSEParams | StdioParams | WebsocketParams,
192
+ connection_params: StreamableHttpParams | SSEParams | StdioParams | WebsocketParams,
137
193
  max_retries: int = 3,
138
194
  mount_points: list[str | MCPRoot] | None = None,
139
195
  timeout_seconds: int | float = 10,
@@ -175,6 +231,9 @@ class FlockFactory:
175
231
  if isinstance(connection_params, FlockFactory.WebsocketParams):
176
232
  server_kind = "websockets"
177
233
  concrete_server_cls = FlockWSServer
234
+ if isinstance(connection_params, FlockFactory.StreamableHttpParams):
235
+ server_kind = "streamable_http"
236
+ concrete_server_cls = FlockStreamableHttpServer
178
237
 
179
238
  # convert mount points.
180
239
  mounts: list[MCPRoot] = []
@@ -243,12 +302,37 @@ class FlockFactory:
243
302
  caching_config=caching_config,
244
303
  callback_config=callback_config,
245
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
+
246
329
  elif server_kind == "sse":
247
330
  # build sse config
248
331
  connection_config = FlockSSEConnectionConfig(
249
332
  max_retries=max_retries,
250
333
  connection_parameters=SseServerParameters(
251
334
  url=connection_params.url,
335
+ auth=connection_params.auth,
252
336
  headers=connection_params.headers,
253
337
  timeout=connection_params.timeout_seconds,
254
338
  sse_read_timeout=connection_params.sse_read_timeout_seconds,
@@ -381,3 +465,46 @@ class FlockFactory:
381
465
  agent.add_module(output_module)
382
466
  agent.add_module(metrics_module)
383
467
  return agent
468
+
469
+ @staticmethod
470
+ def create_scheduled_agent(
471
+ name: str,
472
+ schedule_expression: str, # e.g., "every 1h", "0 0 * * *"
473
+ description: str | Callable[..., str] | None = None,
474
+ model: str | Callable[..., str] | None = None,
475
+ input: SignatureType = None, # Input might be implicit or none
476
+ output: SignatureType = None, # Input might be implicit or none
477
+ tools: list[Callable[..., Any] | Any] | None = None,
478
+ servers: list[str | FlockMCPServerBase] | None = None,
479
+ use_cache: bool = False, # Whether to cache results
480
+ temperature: float = 0.7, # Temperature for model responses
481
+ # ... other common agent params from create_default_agent ...
482
+ temporal_activity_config: TemporalActivityConfig | None = None, # If you want scheduled tasks to be Temporal activities
483
+ **kwargs # Forward other standard agent params
484
+ ) -> FlockAgent:
485
+ """Creates a FlockAgent configured to run on a schedule."""
486
+ agent_config = ScheduledAgentConfig( # Use the new config type
487
+ schedule_expression=schedule_expression,
488
+ enabled=True,
489
+ initial_run=True,
490
+ max_runs=0,
491
+ **kwargs
492
+ )
493
+
494
+
495
+ agent = FlockFactory.create_default_agent( # Reuse your existing factory
496
+ name=name,
497
+ description=description,
498
+ model=model,
499
+ input=input + ", trigger_time: str | Time of scheduled execution",
500
+ output=output,
501
+ tools=tools,
502
+ servers=servers,
503
+ temporal_activity_config=temporal_activity_config,
504
+ use_cache=use_cache,
505
+ temperature=temperature,
506
+ **kwargs
507
+ )
508
+ agent.config = agent_config # Assign the scheduled agent config
509
+
510
+ return agent
@@ -0,0 +1,166 @@
1
+ # src/flock/core/scheduler.py (new file or inside flock.py)
2
+ import asyncio
3
+ import traceback
4
+ from datetime import datetime, timedelta, timezone
5
+
6
+ from croniter import croniter # pip install croniter
7
+
8
+ from flock.core.flock import Flock
9
+ from flock.core.flock_agent import FlockAgent # For type hinting
10
+ from flock.core.logging.logging import get_logger
11
+
12
+ logger = get_logger("flock.scheduler")
13
+
14
+ class FlockScheduler:
15
+ def __init__(self, flock_instance: Flock):
16
+ self.flock = flock_instance
17
+ self._scheduled_tasks: list[tuple[FlockAgent, croniter | timedelta, datetime | None]] = []
18
+ self._stop_event = asyncio.Event()
19
+ self._is_running = False
20
+
21
+ def _parse_schedule_expression(self, expression: str) -> croniter | timedelta:
22
+ """Parses a schedule expression.
23
+ Supports cron syntax and simple intervals like 'every Xm', 'every Xh', 'every Xd'.
24
+ """
25
+ expression = expression.strip().lower()
26
+ if expression.startswith("every "):
27
+ try:
28
+ parts = expression.split(" ")
29
+ value = int(parts[1][:-1])
30
+ unit = parts[1][-1]
31
+ if unit == 's': return timedelta(seconds=value)
32
+ if unit == 'm': return timedelta(minutes=value)
33
+ elif unit == 'h': return timedelta(hours=value)
34
+ elif unit == 'd': return timedelta(days=value)
35
+ else: raise ValueError(f"Invalid time unit: {unit}")
36
+ except Exception as e:
37
+ logger.error(f"Invalid interval expression '{expression}': {e}")
38
+ raise ValueError(f"Invalid interval expression: {expression}") from e
39
+ else:
40
+ # Assume cron expression
41
+ if not croniter.is_valid(expression):
42
+ raise ValueError(f"Invalid cron expression: {expression}")
43
+ return croniter(expression, datetime.now(timezone.utc))
44
+
45
+ def add_agent(self, agent: FlockAgent):
46
+ """Adds an agent to the scheduler if it has a schedule expression."""
47
+ # Assuming schedule_expression is stored in agent._flock_config or a dedicated field
48
+ schedule_expression = None
49
+ if hasattr(agent, '_flock_config') and isinstance(agent._flock_config, dict):
50
+ schedule_expression = agent._flock_config.get('schedule_expression')
51
+ elif hasattr(agent, 'schedule_expression'): # If directly on agent
52
+ schedule_expression = agent.schedule_expression
53
+ elif hasattr(agent, 'config') and hasattr(agent.config, 'schedule_expression'): # If on agent.config
54
+ schedule_expression = agent.config.schedule_expression
55
+
56
+
57
+ if not schedule_expression:
58
+ logger.warning(f"Agent '{agent.name}' has no schedule_expression. Skipping.")
59
+ return
60
+
61
+ try:
62
+ parsed_schedule = self._parse_schedule_expression(schedule_expression)
63
+ self._scheduled_tasks.append((agent, parsed_schedule, None)) # agent, schedule, last_run_utc
64
+ logger.info(f"Scheduled agent '{agent.name}' with expression: {schedule_expression}")
65
+ except ValueError as e:
66
+ logger.error(f"Could not schedule agent '{agent.name}': {e}")
67
+
68
+ def _load_scheduled_agents_from_flock(self):
69
+ """Scans the Flock instance for agents with scheduling configuration."""
70
+ self._scheduled_tasks = [] # Clear existing before loading
71
+ for agent_name, agent_instance in self.flock.agents.items():
72
+ self.add_agent(agent_instance)
73
+ logger.info(f"Loaded {len(self._scheduled_tasks)} scheduled agents from Flock '{self.flock.name}'.")
74
+
75
+ async def _run_agent_task(self, agent: FlockAgent, trigger_time: datetime):
76
+ logger.info(f"Triggering scheduled agent '{agent.name}' at {trigger_time.isoformat()}")
77
+ try:
78
+ # Input for a scheduled agent could include the trigger time
79
+ await self.flock.run_async(start_agent=agent.name, input={"trigger_time": trigger_time})
80
+ logger.info(f"Scheduled agent '{agent.name}' finished successfully.")
81
+ except Exception as e:
82
+ logger.error(f"Error running scheduled agent '{agent.name}': {e}\n{traceback.format_exc()}")
83
+
84
+ async def _scheduler_loop(self):
85
+ self._is_running = True
86
+ logger.info("FlockScheduler loop started.")
87
+ while not self._stop_event.is_set():
88
+ now_utc = datetime.now(timezone.utc)
89
+ tasks_to_run_this_cycle = []
90
+
91
+ for i, (agent, schedule, last_run_utc) in enumerate(self._scheduled_tasks):
92
+ should_run = False
93
+ next_run_utc = None
94
+
95
+ if isinstance(schedule, croniter):
96
+ # For cron, get the next scheduled time AFTER the last run (or now if never run)
97
+ base_time = last_run_utc if last_run_utc else now_utc
98
+ # Croniter's get_next gives the next time *after* the base_time.
99
+ # If last_run_utc is None (first run), we check if the *current* now_utc
100
+ # is past the first scheduled time.
101
+ # A simpler check: is `now_utc` >= `schedule.get_next(datetime, base_time=last_run_utc if last_run_utc else now_utc - timedelta(seconds=1))` ?
102
+ # Let's refine croniter check to be more precise.
103
+ # If we are past the *next* scheduled time since *last check*
104
+ if last_run_utc is None: # First run check
105
+ next_run_utc = schedule.get_next(datetime, start_time=now_utc - timedelta(seconds=1)) # Check if first run is due
106
+ if next_run_utc <= now_utc:
107
+ should_run = True
108
+ else:
109
+ next_run_utc = schedule.get_next(datetime, start_time=last_run_utc)
110
+ if next_run_utc <= now_utc:
111
+ should_run = True
112
+
113
+ elif isinstance(schedule, timedelta): # Simple interval
114
+ if last_run_utc is None or (now_utc - last_run_utc >= schedule):
115
+ should_run = True
116
+ next_run_utc = now_utc # Or now_utc + schedule for next interval start
117
+ else:
118
+ next_run_utc = last_run_utc + schedule
119
+
120
+
121
+ if should_run:
122
+ tasks_to_run_this_cycle.append(self._run_agent_task(agent, now_utc))
123
+ # Update last_run_utc for this agent *before* awaiting its execution
124
+ # For cron, advance the iterator to the *current* scheduled time that triggered it.
125
+ if isinstance(schedule, croniter):
126
+ current_cron_trigger = schedule.get_current(datetime) # This is the time it *should* have run
127
+ self._scheduled_tasks[i] = (agent, schedule, current_cron_trigger)
128
+
129
+ elif isinstance(schedule, timedelta):
130
+ self._scheduled_tasks[i] = (agent, schedule, now_utc) # Mark as run now
131
+
132
+ if tasks_to_run_this_cycle:
133
+ await asyncio.gather(*tasks_to_run_this_cycle, return_exceptions=True)
134
+
135
+ try:
136
+ # Sleep for a short interval, e.g., 10 seconds, or until stop_event is set
137
+ await asyncio.wait_for(self._stop_event.wait(), timeout=10.0)
138
+ except asyncio.TimeoutError:
139
+ pass # Timeout is normal, means continue loop
140
+ self._is_running = False
141
+ logger.info("FlockScheduler loop stopped.")
142
+
143
+ async def start(self) -> asyncio.Task | None: # Modified to return Task or None
144
+ if self._is_running:
145
+ logger.warning("Scheduler is already running.")
146
+ return None # Or return the existing task if you store it
147
+
148
+ self._load_scheduled_agents_from_flock()
149
+ if not self._scheduled_tasks:
150
+ logger.info("No scheduled agents found. Scheduler will not start a loop task.")
151
+ return None # Return None if no tasks to schedule
152
+
153
+ self._stop_event.clear()
154
+ loop_task = asyncio.create_task(self._scheduler_loop())
155
+ # Store the task if you need to reference it, e.g., for forced cancellation beyond _stop_event
156
+ # self._loop_task = loop_task
157
+ return loop_task # Return the created task
158
+
159
+ async def stop(self):
160
+ if not self._is_running and not self._stop_event.is_set(): # Check if stop already called
161
+ logger.info("Scheduler is not running or already signaled to stop.")
162
+ return
163
+ logger.info("Stopping FlockScheduler...")
164
+ self._stop_event.set()
165
+ # If you stored self._loop_task, you can await it here or in the lifespan manager
166
+ # await self._loop_task # (This might block if loop doesn't exit quickly)
@@ -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")