flock-core 0.5.0b63__py3-none-any.whl → 0.5.0b65__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/agent.py +205 -27
- flock/dashboard/websocket.py +13 -2
- flock/engines/dspy_engine.py +30 -9
- flock/frontend/package-lock.json +11 -21
- flock/mcp/client.py +25 -1
- flock/mcp/config.py +1 -10
- flock/mcp/manager.py +34 -3
- flock/mcp/types/callbacks.py +4 -1
- flock/orchestrator.py +33 -4
- {flock_core-0.5.0b63.dist-info → flock_core-0.5.0b65.dist-info}/METADATA +2 -1
- {flock_core-0.5.0b63.dist-info → flock_core-0.5.0b65.dist-info}/RECORD +14 -14
- {flock_core-0.5.0b63.dist-info → flock_core-0.5.0b65.dist-info}/WHEEL +0 -0
- {flock_core-0.5.0b63.dist-info → flock_core-0.5.0b65.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.0b63.dist-info → flock_core-0.5.0b65.dist-info}/licenses/LICENSE +0 -0
flock/agent.py
CHANGED
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import os
|
|
7
7
|
from dataclasses import dataclass
|
|
8
|
-
from typing import TYPE_CHECKING, Any
|
|
8
|
+
from typing import TYPE_CHECKING, Any, TypedDict
|
|
9
9
|
|
|
10
10
|
from pydantic import BaseModel
|
|
11
11
|
|
|
@@ -27,6 +27,38 @@ if TYPE_CHECKING: # pragma: no cover - type hints only
|
|
|
27
27
|
from flock.orchestrator import Flock
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
class MCPServerConfig(TypedDict, total=False):
|
|
31
|
+
"""Configuration for MCP server assignment to an agent.
|
|
32
|
+
|
|
33
|
+
All fields are optional. If omitted, no restrictions apply.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
roots: Filesystem paths this server can access.
|
|
37
|
+
Empty list or omitted = no mount restrictions.
|
|
38
|
+
tool_whitelist: Tool names the agent can use from this server.
|
|
39
|
+
Empty list or omitted = all tools available.
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
>>> # No restrictions
|
|
43
|
+
>>> config: MCPServerConfig = {}
|
|
44
|
+
|
|
45
|
+
>>> # Mount restrictions only
|
|
46
|
+
>>> config: MCPServerConfig = {"roots": ["/workspace/data"]}
|
|
47
|
+
|
|
48
|
+
>>> # Tool whitelist only
|
|
49
|
+
>>> config: MCPServerConfig = {"tool_whitelist": ["read_file", "write_file"]}
|
|
50
|
+
|
|
51
|
+
>>> # Both restrictions
|
|
52
|
+
>>> config: MCPServerConfig = {
|
|
53
|
+
... "roots": ["/workspace/data"],
|
|
54
|
+
... "tool_whitelist": ["read_file"]
|
|
55
|
+
... }
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
roots: list[str]
|
|
59
|
+
tool_whitelist: list[str]
|
|
60
|
+
|
|
61
|
+
|
|
30
62
|
@dataclass
|
|
31
63
|
class AgentOutput:
|
|
32
64
|
spec: ArtifactSpec
|
|
@@ -67,7 +99,7 @@ class Agent(metaclass=AutoTracedMeta):
|
|
|
67
99
|
self.engines: list[EngineComponent] = []
|
|
68
100
|
self.best_of_n: int = 1
|
|
69
101
|
self.best_of_score: Callable[[EvalResult], float] | None = None
|
|
70
|
-
self.max_concurrency: int =
|
|
102
|
+
self.max_concurrency: int = 2
|
|
71
103
|
self._semaphore = asyncio.Semaphore(self.max_concurrency)
|
|
72
104
|
self.calls_func: Callable[..., Any] | None = None
|
|
73
105
|
self.tools: set[Callable[..., Any]] = set()
|
|
@@ -77,6 +109,9 @@ class Agent(metaclass=AutoTracedMeta):
|
|
|
77
109
|
self.prevent_self_trigger: bool = True # T065: Prevent infinite feedback loops
|
|
78
110
|
# MCP integration
|
|
79
111
|
self.mcp_server_names: set[str] = set()
|
|
112
|
+
self.mcp_mount_points: list[str] = [] # Deprecated: Use mcp_server_mounts instead
|
|
113
|
+
self.mcp_server_mounts: dict[str, list[str]] = {} # Server-specific mount points
|
|
114
|
+
self.tool_whitelist: list[str] | None = None
|
|
80
115
|
|
|
81
116
|
@property
|
|
82
117
|
def identity(self) -> AgentIdentity:
|
|
@@ -137,15 +172,30 @@ class Agent(metaclass=AutoTracedMeta):
|
|
|
137
172
|
# Get the MCP manager from orchestrator
|
|
138
173
|
manager = self._orchestrator.get_mcp_manager()
|
|
139
174
|
|
|
140
|
-
# Import tool wrapper
|
|
141
|
-
|
|
142
175
|
# Fetch tools from all assigned servers
|
|
143
176
|
tools_dict = await manager.get_tools_for_agent(
|
|
144
177
|
agent_id=self.name,
|
|
145
178
|
run_id=ctx.task_id,
|
|
146
179
|
server_names=self.mcp_server_names,
|
|
180
|
+
server_mounts=self.mcp_server_mounts, # Pass server-specific mounts
|
|
147
181
|
)
|
|
148
182
|
|
|
183
|
+
# Whitelisting logic
|
|
184
|
+
tool_whitelist = self.tool_whitelist
|
|
185
|
+
if (
|
|
186
|
+
tool_whitelist is not None
|
|
187
|
+
and isinstance(tool_whitelist, list)
|
|
188
|
+
and len(tool_whitelist) > 0
|
|
189
|
+
):
|
|
190
|
+
filtered_tools: dict[str, Any] = {}
|
|
191
|
+
for tool_key, tool_entry in tools_dict.items():
|
|
192
|
+
if isinstance(tool_entry, dict):
|
|
193
|
+
original_name = tool_entry.get("original_name", None)
|
|
194
|
+
if original_name is not None and original_name in tool_whitelist:
|
|
195
|
+
filtered_tools[tool_key] = tool_entry
|
|
196
|
+
|
|
197
|
+
tools_dict = filtered_tools
|
|
198
|
+
|
|
149
199
|
# Convert to DSPy tool callables
|
|
150
200
|
dspy_tools = []
|
|
151
201
|
for namespaced_name, tool_info in tools_dict.items():
|
|
@@ -630,30 +680,103 @@ class AgentBuilder:
|
|
|
630
680
|
self._agent.tools.update(funcs)
|
|
631
681
|
return self
|
|
632
682
|
|
|
633
|
-
def with_mcps(
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
683
|
+
def with_mcps(
|
|
684
|
+
self,
|
|
685
|
+
servers: (
|
|
686
|
+
Iterable[str]
|
|
687
|
+
| dict[str, MCPServerConfig | list[str]] # Support both new and old format
|
|
688
|
+
| list[str | dict[str, MCPServerConfig | list[str]]]
|
|
689
|
+
),
|
|
690
|
+
) -> AgentBuilder:
|
|
691
|
+
"""Assign MCP servers to this agent with optional server-specific mount points.
|
|
692
|
+
|
|
693
|
+
Architecture Decision: AD001 - Two-Level Architecture
|
|
694
|
+
Agents reference servers registered at orchestrator level.
|
|
695
|
+
|
|
696
|
+
Args:
|
|
697
|
+
servers: One of:
|
|
698
|
+
- List of server names (strings) - no specific mounts
|
|
699
|
+
- Dict mapping server names to MCPServerConfig or list[str] (backward compatible)
|
|
700
|
+
- Mixed list of strings and dicts for flexibility
|
|
701
|
+
|
|
702
|
+
Returns:
|
|
703
|
+
self for method chaining
|
|
704
|
+
|
|
705
|
+
Raises:
|
|
706
|
+
ValueError: If any server name is not registered with orchestrator
|
|
707
|
+
|
|
708
|
+
Examples:
|
|
709
|
+
>>> # Simple: no mount restrictions
|
|
710
|
+
>>> agent.with_mcps(["filesystem", "github"])
|
|
711
|
+
|
|
712
|
+
>>> # New format: Server-specific config with roots and tool whitelist
|
|
713
|
+
>>> agent.with_mcps({
|
|
714
|
+
... "filesystem": {"roots": ["/workspace/dir/data"], "tool_whitelist": ["read_file"]},
|
|
715
|
+
... "github": {} # No restrictions for github
|
|
716
|
+
... })
|
|
717
|
+
|
|
718
|
+
>>> # Old format: Direct list (backward compatible)
|
|
719
|
+
>>> agent.with_mcps({
|
|
720
|
+
... "filesystem": ["/workspace/dir/data"], # Old format still works
|
|
721
|
+
... })
|
|
722
|
+
|
|
723
|
+
>>> # Mixed: backward compatible
|
|
724
|
+
>>> agent.with_mcps([
|
|
725
|
+
... "github", # No mounts
|
|
726
|
+
... {"filesystem": {"roots": ["mount1", "mount2"] } }
|
|
727
|
+
```
|
|
728
|
+
... ])
|
|
654
729
|
"""
|
|
655
|
-
#
|
|
656
|
-
server_set = set(
|
|
730
|
+
# Parse input into server_names and mounts
|
|
731
|
+
server_set: set[str] = set()
|
|
732
|
+
server_mounts: dict[str, list[str]] = {}
|
|
733
|
+
whitelist = None
|
|
734
|
+
|
|
735
|
+
if isinstance(servers, dict):
|
|
736
|
+
# Dict format: supports both old and new formats
|
|
737
|
+
# Old: {"server": ["/path1", "/path2"]}
|
|
738
|
+
# New: {"server": {"roots": ["/path1"], "tool_whitelist": ["tool1"]}}
|
|
739
|
+
for server_name, server_config in servers.items():
|
|
740
|
+
server_set.add(server_name)
|
|
741
|
+
|
|
742
|
+
# Check if it's the old format (direct list) or new format (MCPServerConfig dict)
|
|
743
|
+
if isinstance(server_config, list):
|
|
744
|
+
# Old format: direct list of paths (backward compatibility)
|
|
745
|
+
if len(server_config) > 0:
|
|
746
|
+
server_mounts[server_name] = list(server_config)
|
|
747
|
+
elif isinstance(server_config, dict):
|
|
748
|
+
# New format: MCPServerConfig with optional roots and tool_whitelist
|
|
749
|
+
mounts = server_config.get("roots", None)
|
|
750
|
+
if mounts is not None and isinstance(mounts, list) and len(mounts) > 0:
|
|
751
|
+
server_mounts[server_name] = list(mounts)
|
|
752
|
+
|
|
753
|
+
config_whitelist = server_config.get("tool_whitelist", None)
|
|
754
|
+
if (
|
|
755
|
+
config_whitelist is not None
|
|
756
|
+
and isinstance(config_whitelist, list)
|
|
757
|
+
and len(config_whitelist) > 0
|
|
758
|
+
):
|
|
759
|
+
whitelist = config_whitelist
|
|
760
|
+
elif isinstance(servers, list):
|
|
761
|
+
# List format: can be mixed
|
|
762
|
+
for item in servers:
|
|
763
|
+
if isinstance(item, str):
|
|
764
|
+
# Simple server name
|
|
765
|
+
server_set.add(item)
|
|
766
|
+
elif isinstance(item, dict):
|
|
767
|
+
# Dict with mounts
|
|
768
|
+
for server_name, mounts in item.items():
|
|
769
|
+
server_set.add(server_name)
|
|
770
|
+
if mounts:
|
|
771
|
+
server_mounts[server_name] = list(mounts)
|
|
772
|
+
else:
|
|
773
|
+
raise TypeError(
|
|
774
|
+
f"Invalid server specification: {item}. "
|
|
775
|
+
f"Expected string or dict, got {type(item).__name__}"
|
|
776
|
+
)
|
|
777
|
+
else:
|
|
778
|
+
# Assume it's an iterable of strings (backward compatibility)
|
|
779
|
+
server_set = set(servers)
|
|
657
780
|
|
|
658
781
|
# Validate all servers exist in orchestrator
|
|
659
782
|
registered_servers = set(self._orchestrator._mcp_configs.keys())
|
|
@@ -669,6 +792,61 @@ class AgentBuilder:
|
|
|
669
792
|
|
|
670
793
|
# Store in agent
|
|
671
794
|
self._agent.mcp_server_names = server_set
|
|
795
|
+
self._agent.mcp_server_mounts = server_mounts
|
|
796
|
+
self._agent.tool_whitelist = whitelist
|
|
797
|
+
|
|
798
|
+
return self
|
|
799
|
+
|
|
800
|
+
def mount(self, paths: str | list[str], *, validate: bool = False) -> AgentBuilder:
|
|
801
|
+
"""Mount agent in specific directories for MCP root access.
|
|
802
|
+
|
|
803
|
+
.. deprecated:: 0.2.0
|
|
804
|
+
Use `.with_mcps({"server_name": ["/path"]})` instead for server-specific mounts.
|
|
805
|
+
This method applies mounts globally to all MCP servers.
|
|
806
|
+
|
|
807
|
+
This sets the filesystem roots that MCP servers will operate under for this agent.
|
|
808
|
+
Paths are cumulative across multiple calls.
|
|
809
|
+
|
|
810
|
+
Args:
|
|
811
|
+
paths: Single path or list of paths to mount
|
|
812
|
+
validate: If True, validate that paths exist (default: False)
|
|
813
|
+
|
|
814
|
+
Returns:
|
|
815
|
+
AgentBuilder for method chaining
|
|
816
|
+
|
|
817
|
+
Example:
|
|
818
|
+
>>> # Old way (deprecated)
|
|
819
|
+
>>> agent.with_mcps(["filesystem"]).mount("/workspace/src")
|
|
820
|
+
>>>
|
|
821
|
+
>>> # New way (recommended)
|
|
822
|
+
>>> agent.with_mcps({"filesystem": ["/workspace/src"]})
|
|
823
|
+
"""
|
|
824
|
+
import warnings
|
|
825
|
+
|
|
826
|
+
warnings.warn(
|
|
827
|
+
"Agent.mount() is deprecated. Use .with_mcps({'server': ['/path']}) "
|
|
828
|
+
"for server-specific mounts instead.",
|
|
829
|
+
DeprecationWarning,
|
|
830
|
+
stacklevel=2,
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
if isinstance(paths, str):
|
|
834
|
+
paths = [paths]
|
|
835
|
+
if validate:
|
|
836
|
+
from pathlib import Path
|
|
837
|
+
|
|
838
|
+
for path in paths:
|
|
839
|
+
if not Path(path).exists():
|
|
840
|
+
raise ValueError(f"Mount path does not exist: {path}")
|
|
841
|
+
|
|
842
|
+
# Add to agent's mount points (cumulative) - for backward compatibility
|
|
843
|
+
self._agent.mcp_mount_points.extend(paths)
|
|
844
|
+
|
|
845
|
+
# Also add to all configured servers for backward compatibility
|
|
846
|
+
for server_name in self._agent.mcp_server_names:
|
|
847
|
+
if server_name not in self._agent.mcp_server_mounts:
|
|
848
|
+
self._agent.mcp_server_mounts[server_name] = []
|
|
849
|
+
self._agent.mcp_server_mounts[server_name].extend(paths)
|
|
672
850
|
|
|
673
851
|
return self
|
|
674
852
|
|
flock/dashboard/websocket.py
CHANGED
|
@@ -124,15 +124,26 @@ class WebSocketManager:
|
|
|
124
124
|
# Broadcast to all clients concurrently
|
|
125
125
|
# Use return_exceptions=True to handle client failures gracefully
|
|
126
126
|
# Use send_text() for FastAPI WebSocket (send JSON string as text)
|
|
127
|
+
# CRITICAL: Add timeout to prevent deadlock when client send buffer is full
|
|
127
128
|
clients_list = list(self.clients) # Copy to avoid modification during iteration
|
|
128
|
-
|
|
129
|
+
|
|
130
|
+
send_tasks = [
|
|
131
|
+
asyncio.wait_for(client.send_text(message), timeout=0.5) # 500ms timeout
|
|
132
|
+
for client in clients_list
|
|
133
|
+
]
|
|
129
134
|
results = await asyncio.gather(*send_tasks, return_exceptions=True)
|
|
130
135
|
|
|
131
136
|
# Remove clients that failed to receive the message
|
|
132
137
|
failed_clients = []
|
|
133
138
|
for client, result in zip(clients_list, results, strict=False):
|
|
134
139
|
if isinstance(result, Exception):
|
|
135
|
-
|
|
140
|
+
# Check if it's a timeout (backpressure) or other error
|
|
141
|
+
if isinstance(result, asyncio.TimeoutError):
|
|
142
|
+
logger.warning(
|
|
143
|
+
"Client send timeout (backpressure) - client is slow or disconnected, removing client"
|
|
144
|
+
)
|
|
145
|
+
else:
|
|
146
|
+
logger.warning(f"Failed to send to client: {result}")
|
|
136
147
|
failed_clients.append(client)
|
|
137
148
|
|
|
138
149
|
# Clean up failed clients
|
flock/engines/dspy_engine.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
5
6
|
import json
|
|
6
7
|
import os
|
|
7
8
|
from collections import OrderedDict, defaultdict
|
|
@@ -562,6 +563,9 @@ class DSPyEngine(EngineComponent):
|
|
|
562
563
|
stream_buffers[status_field] = []
|
|
563
564
|
stream_sequence = 0 # Monotonic sequence for ordering
|
|
564
565
|
|
|
566
|
+
# Track background WebSocket broadcast tasks to prevent garbage collection
|
|
567
|
+
ws_broadcast_tasks: set[asyncio.Task] = set()
|
|
568
|
+
|
|
565
569
|
formatter = theme_dict = styles = agent_label = None
|
|
566
570
|
live_cm = nullcontext()
|
|
567
571
|
overflow_mode = self.stream_vertical_overflow
|
|
@@ -607,7 +611,7 @@ class DSPyEngine(EngineComponent):
|
|
|
607
611
|
stream_buffers[status_field].append(str(token) + "\n")
|
|
608
612
|
display_data["status"] = "".join(stream_buffers[status_field])
|
|
609
613
|
|
|
610
|
-
# Emit to WebSocket
|
|
614
|
+
# Emit to WebSocket (non-blocking to prevent deadlock)
|
|
611
615
|
if ws_manager and token:
|
|
612
616
|
try:
|
|
613
617
|
event = StreamingOutputEvent(
|
|
@@ -621,10 +625,15 @@ class DSPyEngine(EngineComponent):
|
|
|
621
625
|
sequence=stream_sequence,
|
|
622
626
|
is_final=False,
|
|
623
627
|
)
|
|
624
|
-
|
|
628
|
+
# Use create_task to avoid blocking the streaming loop
|
|
629
|
+
task = asyncio.create_task(ws_manager.broadcast(event))
|
|
630
|
+
ws_broadcast_tasks.add(task)
|
|
631
|
+
task.add_done_callback(ws_broadcast_tasks.discard)
|
|
625
632
|
stream_sequence += 1
|
|
626
633
|
except Exception as e:
|
|
627
634
|
logger.warning(f"Failed to emit streaming event: {e}")
|
|
635
|
+
else:
|
|
636
|
+
logger.exception("NO WS_MANAGER PRESENT!!!!")
|
|
628
637
|
|
|
629
638
|
if formatter is not None:
|
|
630
639
|
_refresh_panel()
|
|
@@ -643,7 +652,7 @@ class DSPyEngine(EngineComponent):
|
|
|
643
652
|
stream_buffers[buffer_key]
|
|
644
653
|
)
|
|
645
654
|
|
|
646
|
-
# Emit to WebSocket
|
|
655
|
+
# Emit to WebSocket (non-blocking to prevent deadlock)
|
|
647
656
|
if ws_manager:
|
|
648
657
|
logger.info(
|
|
649
658
|
f"[STREAMING] Emitting StreamResponse token='{token}', sequence={stream_sequence}"
|
|
@@ -660,7 +669,10 @@ class DSPyEngine(EngineComponent):
|
|
|
660
669
|
sequence=stream_sequence,
|
|
661
670
|
is_final=False,
|
|
662
671
|
)
|
|
663
|
-
|
|
672
|
+
# Use create_task to avoid blocking the streaming loop
|
|
673
|
+
task = asyncio.create_task(ws_manager.broadcast(event))
|
|
674
|
+
ws_broadcast_tasks.add(task)
|
|
675
|
+
task.add_done_callback(ws_broadcast_tasks.discard)
|
|
664
676
|
stream_sequence += 1
|
|
665
677
|
except Exception as e:
|
|
666
678
|
logger.warning(f"Failed to emit streaming event: {e}")
|
|
@@ -690,7 +702,7 @@ class DSPyEngine(EngineComponent):
|
|
|
690
702
|
stream_buffers[status_field].append(str(token))
|
|
691
703
|
display_data["status"] = "".join(stream_buffers[status_field])
|
|
692
704
|
|
|
693
|
-
# Emit to WebSocket
|
|
705
|
+
# Emit to WebSocket (non-blocking to prevent deadlock)
|
|
694
706
|
if ws_manager and token:
|
|
695
707
|
try:
|
|
696
708
|
event = StreamingOutputEvent(
|
|
@@ -704,7 +716,10 @@ class DSPyEngine(EngineComponent):
|
|
|
704
716
|
sequence=stream_sequence,
|
|
705
717
|
is_final=False,
|
|
706
718
|
)
|
|
707
|
-
|
|
719
|
+
# Use create_task to avoid blocking the streaming loop
|
|
720
|
+
task = asyncio.create_task(ws_manager.broadcast(event))
|
|
721
|
+
ws_broadcast_tasks.add(task)
|
|
722
|
+
task.add_done_callback(ws_broadcast_tasks.discard)
|
|
708
723
|
stream_sequence += 1
|
|
709
724
|
except Exception as e:
|
|
710
725
|
logger.warning(f"Failed to emit streaming event: {e}")
|
|
@@ -716,7 +731,7 @@ class DSPyEngine(EngineComponent):
|
|
|
716
731
|
if isinstance(value, dspy_mod.Prediction):
|
|
717
732
|
final_result = value
|
|
718
733
|
|
|
719
|
-
# Emit final streaming event
|
|
734
|
+
# Emit final streaming event (non-blocking to prevent deadlock)
|
|
720
735
|
if ws_manager:
|
|
721
736
|
try:
|
|
722
737
|
event = StreamingOutputEvent(
|
|
@@ -730,7 +745,10 @@ class DSPyEngine(EngineComponent):
|
|
|
730
745
|
sequence=stream_sequence,
|
|
731
746
|
is_final=True, # Mark as final
|
|
732
747
|
)
|
|
733
|
-
|
|
748
|
+
# Use create_task to avoid blocking the streaming loop
|
|
749
|
+
task = asyncio.create_task(ws_manager.broadcast(event))
|
|
750
|
+
ws_broadcast_tasks.add(task)
|
|
751
|
+
task.add_done_callback(ws_broadcast_tasks.discard)
|
|
734
752
|
event = StreamingOutputEvent(
|
|
735
753
|
correlation_id=str(ctx.correlation_id)
|
|
736
754
|
if ctx and ctx.correlation_id
|
|
@@ -742,7 +760,10 @@ class DSPyEngine(EngineComponent):
|
|
|
742
760
|
sequence=stream_sequence,
|
|
743
761
|
is_final=True, # Mark as final
|
|
744
762
|
)
|
|
745
|
-
|
|
763
|
+
# Use create_task to avoid blocking the streaming loop
|
|
764
|
+
task = asyncio.create_task(ws_manager.broadcast(event))
|
|
765
|
+
ws_broadcast_tasks.add(task)
|
|
766
|
+
task.add_done_callback(ws_broadcast_tasks.discard)
|
|
746
767
|
except Exception as e:
|
|
747
768
|
logger.warning(f"Failed to emit final streaming event: {e}")
|
|
748
769
|
|
flock/frontend/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flock-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "flock-ui",
|
|
9
|
-
"version": "0.1.
|
|
9
|
+
"version": "0.1.4",
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@types/dagre": "^0.7.53",
|
|
@@ -147,7 +147,6 @@
|
|
|
147
147
|
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
|
148
148
|
"dev": true,
|
|
149
149
|
"license": "MIT",
|
|
150
|
-
"peer": true,
|
|
151
150
|
"dependencies": {
|
|
152
151
|
"@babel/code-frame": "^7.27.1",
|
|
153
152
|
"@babel/generator": "^7.28.3",
|
|
@@ -356,7 +355,6 @@
|
|
|
356
355
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
|
357
356
|
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
|
358
357
|
"license": "MIT",
|
|
359
|
-
"peer": true,
|
|
360
358
|
"engines": {
|
|
361
359
|
"node": ">=6.9.0"
|
|
362
360
|
}
|
|
@@ -507,7 +505,6 @@
|
|
|
507
505
|
}
|
|
508
506
|
],
|
|
509
507
|
"license": "MIT",
|
|
510
|
-
"peer": true,
|
|
511
508
|
"engines": {
|
|
512
509
|
"node": ">=18"
|
|
513
510
|
},
|
|
@@ -554,7 +551,6 @@
|
|
|
554
551
|
}
|
|
555
552
|
],
|
|
556
553
|
"license": "MIT",
|
|
557
|
-
"peer": true,
|
|
558
554
|
"engines": {
|
|
559
555
|
"node": ">=18"
|
|
560
556
|
}
|
|
@@ -1507,7 +1503,8 @@
|
|
|
1507
1503
|
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
|
1508
1504
|
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
|
1509
1505
|
"dev": true,
|
|
1510
|
-
"license": "MIT"
|
|
1506
|
+
"license": "MIT",
|
|
1507
|
+
"peer": true
|
|
1511
1508
|
},
|
|
1512
1509
|
"node_modules/@types/babel__core": {
|
|
1513
1510
|
"version": "7.20.5",
|
|
@@ -1638,7 +1635,6 @@
|
|
|
1638
1635
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz",
|
|
1639
1636
|
"integrity": "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==",
|
|
1640
1637
|
"license": "MIT",
|
|
1641
|
-
"peer": true,
|
|
1642
1638
|
"dependencies": {
|
|
1643
1639
|
"csstype": "^3.0.2"
|
|
1644
1640
|
}
|
|
@@ -1649,7 +1645,6 @@
|
|
|
1649
1645
|
"integrity": "sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg==",
|
|
1650
1646
|
"dev": true,
|
|
1651
1647
|
"license": "MIT",
|
|
1652
|
-
"peer": true,
|
|
1653
1648
|
"peerDependencies": {
|
|
1654
1649
|
"@types/react": "^19.2.0"
|
|
1655
1650
|
}
|
|
@@ -1845,7 +1840,6 @@
|
|
|
1845
1840
|
"integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==",
|
|
1846
1841
|
"dev": true,
|
|
1847
1842
|
"license": "MIT",
|
|
1848
|
-
"peer": true,
|
|
1849
1843
|
"dependencies": {
|
|
1850
1844
|
"@vitest/utils": "3.2.4",
|
|
1851
1845
|
"fflate": "^0.8.2",
|
|
@@ -1963,6 +1957,7 @@
|
|
|
1963
1957
|
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
|
1964
1958
|
"dev": true,
|
|
1965
1959
|
"license": "MIT",
|
|
1960
|
+
"peer": true,
|
|
1966
1961
|
"engines": {
|
|
1967
1962
|
"node": ">=10"
|
|
1968
1963
|
},
|
|
@@ -2066,7 +2061,6 @@
|
|
|
2066
2061
|
}
|
|
2067
2062
|
],
|
|
2068
2063
|
"license": "MIT",
|
|
2069
|
-
"peer": true,
|
|
2070
2064
|
"dependencies": {
|
|
2071
2065
|
"baseline-browser-mapping": "^2.8.9",
|
|
2072
2066
|
"caniuse-lite": "^1.0.30001746",
|
|
@@ -2295,7 +2289,6 @@
|
|
|
2295
2289
|
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
|
2296
2290
|
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
|
2297
2291
|
"license": "ISC",
|
|
2298
|
-
"peer": true,
|
|
2299
2292
|
"engines": {
|
|
2300
2293
|
"node": ">=12"
|
|
2301
2294
|
}
|
|
@@ -2418,7 +2411,8 @@
|
|
|
2418
2411
|
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
|
2419
2412
|
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
|
2420
2413
|
"dev": true,
|
|
2421
|
-
"license": "MIT"
|
|
2414
|
+
"license": "MIT",
|
|
2415
|
+
"peer": true
|
|
2422
2416
|
},
|
|
2423
2417
|
"node_modules/eastasianwidth": {
|
|
2424
2418
|
"version": "0.2.0",
|
|
@@ -2840,7 +2834,6 @@
|
|
|
2840
2834
|
"integrity": "sha512-lIHeR1qlIRrIN5VMccd8tI2Sgw6ieYXSVktcSHaNe3Z5nE/tcPQYQWOq00wxMvYOsz+73eAkNenVvmPC6bba9A==",
|
|
2841
2835
|
"dev": true,
|
|
2842
2836
|
"license": "MIT",
|
|
2843
|
-
"peer": true,
|
|
2844
2837
|
"dependencies": {
|
|
2845
2838
|
"@asamuzakjp/dom-selector": "^6.5.4",
|
|
2846
2839
|
"cssstyle": "^5.3.0",
|
|
@@ -2942,6 +2935,7 @@
|
|
|
2942
2935
|
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
|
2943
2936
|
"dev": true,
|
|
2944
2937
|
"license": "MIT",
|
|
2938
|
+
"peer": true,
|
|
2945
2939
|
"bin": {
|
|
2946
2940
|
"lz-string": "bin/bin.js"
|
|
2947
2941
|
}
|
|
@@ -3186,7 +3180,6 @@
|
|
|
3186
3180
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
|
3187
3181
|
"dev": true,
|
|
3188
3182
|
"license": "MIT",
|
|
3189
|
-
"peer": true,
|
|
3190
3183
|
"engines": {
|
|
3191
3184
|
"node": ">=12"
|
|
3192
3185
|
},
|
|
@@ -3214,7 +3207,6 @@
|
|
|
3214
3207
|
}
|
|
3215
3208
|
],
|
|
3216
3209
|
"license": "MIT",
|
|
3217
|
-
"peer": true,
|
|
3218
3210
|
"dependencies": {
|
|
3219
3211
|
"nanoid": "^3.3.11",
|
|
3220
3212
|
"picocolors": "^1.1.1",
|
|
@@ -3230,6 +3222,7 @@
|
|
|
3230
3222
|
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
|
3231
3223
|
"dev": true,
|
|
3232
3224
|
"license": "MIT",
|
|
3225
|
+
"peer": true,
|
|
3233
3226
|
"dependencies": {
|
|
3234
3227
|
"ansi-regex": "^5.0.1",
|
|
3235
3228
|
"ansi-styles": "^5.0.0",
|
|
@@ -3281,7 +3274,6 @@
|
|
|
3281
3274
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
|
3282
3275
|
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
|
3283
3276
|
"license": "MIT",
|
|
3284
|
-
"peer": true,
|
|
3285
3277
|
"engines": {
|
|
3286
3278
|
"node": ">=0.10.0"
|
|
3287
3279
|
}
|
|
@@ -3291,7 +3283,6 @@
|
|
|
3291
3283
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
|
3292
3284
|
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
|
3293
3285
|
"license": "MIT",
|
|
3294
|
-
"peer": true,
|
|
3295
3286
|
"dependencies": {
|
|
3296
3287
|
"scheduler": "^0.27.0"
|
|
3297
3288
|
},
|
|
@@ -3318,7 +3309,8 @@
|
|
|
3318
3309
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
|
3319
3310
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
|
3320
3311
|
"dev": true,
|
|
3321
|
-
"license": "MIT"
|
|
3312
|
+
"license": "MIT",
|
|
3313
|
+
"peer": true
|
|
3322
3314
|
},
|
|
3323
3315
|
"node_modules/react-refresh": {
|
|
3324
3316
|
"version": "0.17.0",
|
|
@@ -3907,7 +3899,6 @@
|
|
|
3907
3899
|
"integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==",
|
|
3908
3900
|
"dev": true,
|
|
3909
3901
|
"license": "MIT",
|
|
3910
|
-
"peer": true,
|
|
3911
3902
|
"dependencies": {
|
|
3912
3903
|
"esbuild": "^0.25.0",
|
|
3913
3904
|
"fdir": "^6.5.0",
|
|
@@ -4006,7 +3997,6 @@
|
|
|
4006
3997
|
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
|
4007
3998
|
"dev": true,
|
|
4008
3999
|
"license": "MIT",
|
|
4009
|
-
"peer": true,
|
|
4010
4000
|
"dependencies": {
|
|
4011
4001
|
"@types/chai": "^5.2.2",
|
|
4012
4002
|
"@vitest/expect": "3.2.4",
|
flock/mcp/client.py
CHANGED
|
@@ -153,6 +153,9 @@ class FlockMCPClient(BaseModel, ABC):
|
|
|
153
153
|
class _SessionProxy:
|
|
154
154
|
def __init__(self, client: Any):
|
|
155
155
|
self._client = client
|
|
156
|
+
# Check if roots are specified in the config
|
|
157
|
+
if not self.current_roots and self.config.connection_config.mount_points:
|
|
158
|
+
self.current_roots = self.config.connection_config.mount_points
|
|
156
159
|
|
|
157
160
|
def __getattr__(self, name: str):
|
|
158
161
|
# return an async function that auto-reconnects, then calls through.
|
|
@@ -366,11 +369,20 @@ class FlockMCPClient(BaseModel, ABC):
|
|
|
366
369
|
return result
|
|
367
370
|
|
|
368
371
|
async def _get_tools_internal() -> list[FlockMCPTool]:
|
|
369
|
-
# TODO: Crash
|
|
370
372
|
response: ListToolsResult = await self.session.list_tools()
|
|
371
373
|
flock_tools = []
|
|
374
|
+
tool_whitelist = self.config.feature_config.tool_whitelist
|
|
372
375
|
|
|
373
376
|
for tool in response.tools:
|
|
377
|
+
# Skip tools that are not whitelisted
|
|
378
|
+
# IF a whitelist is present
|
|
379
|
+
if (
|
|
380
|
+
tool_whitelist is not None
|
|
381
|
+
and isinstance(tool_whitelist, list)
|
|
382
|
+
and len(tool_whitelist) > 0
|
|
383
|
+
and tool.name not in tool_whitelist
|
|
384
|
+
):
|
|
385
|
+
continue
|
|
374
386
|
converted_tool = FlockMCPTool.from_mcp_tool(
|
|
375
387
|
tool,
|
|
376
388
|
agent_id=agent_id,
|
|
@@ -422,6 +434,14 @@ class FlockMCPClient(BaseModel, ABC):
|
|
|
422
434
|
async with self.lock:
|
|
423
435
|
return self.current_roots
|
|
424
436
|
|
|
437
|
+
def _get_roots_no_lock(self) -> list[MCPRoot] | None:
|
|
438
|
+
"""Get the currently set roots without acquiring a lock.
|
|
439
|
+
|
|
440
|
+
WARNING: Only use this internally when you're sure there's no race condition.
|
|
441
|
+
This is primarily for use during initialization when the lock is already held.
|
|
442
|
+
"""
|
|
443
|
+
return self.current_roots
|
|
444
|
+
|
|
425
445
|
async def set_roots(self, new_roots: list[MCPRoot]) -> None:
|
|
426
446
|
"""Set the current roots of the client.
|
|
427
447
|
|
|
@@ -599,6 +619,10 @@ class FlockMCPClient(BaseModel, ABC):
|
|
|
599
619
|
# 2) if we already know our current roots, notify the server
|
|
600
620
|
# so that it will follow up with a ListRootsRequest
|
|
601
621
|
if self.current_roots and self.config.feature_config.roots_enabled:
|
|
622
|
+
logger.debug(
|
|
623
|
+
f"Notifying server '{self.config.name}' of {len(self.current_roots)} root(s): "
|
|
624
|
+
f"{[r.uri for r in self.current_roots]}"
|
|
625
|
+
)
|
|
602
626
|
await self.client_session.send_roots_list_changed()
|
|
603
627
|
|
|
604
628
|
# 3) Tell the server, what logging level we would like to use
|
flock/mcp/config.py
CHANGED
|
@@ -271,7 +271,7 @@ class FlockMCPFeatureConfiguration(BaseModel):
|
|
|
271
271
|
default=None,
|
|
272
272
|
description="Whitelist of tool names that are enabled for this MCP server. "
|
|
273
273
|
"If provided, only tools with names in this list will be available "
|
|
274
|
-
"from this server.
|
|
274
|
+
"from this server."
|
|
275
275
|
"Note: Agent-level tool filtering is generally preferred over "
|
|
276
276
|
"server-level filtering for better granular control.",
|
|
277
277
|
)
|
|
@@ -313,15 +313,6 @@ class FlockMCPConfiguration(BaseModel):
|
|
|
313
313
|
|
|
314
314
|
name: str = Field(..., description="Name of the server the client connects to.")
|
|
315
315
|
|
|
316
|
-
allow_all_tools: bool = Field(
|
|
317
|
-
default=True,
|
|
318
|
-
description="Whether to allow usage of all tools from this MCP server. "
|
|
319
|
-
"When True (default), all tools are available unless restricted "
|
|
320
|
-
"by tool_whitelist. When False, tool access is controlled entirely "
|
|
321
|
-
"by tool_whitelist (if provided). Setting to False with no whitelist "
|
|
322
|
-
"will block all tools from this server.",
|
|
323
|
-
)
|
|
324
|
-
|
|
325
316
|
connection_config: FlockMCPConnectionConfiguration = Field(
|
|
326
317
|
..., description="MCP Connection Configuration for a client."
|
|
327
318
|
)
|
flock/mcp/manager.py
CHANGED
|
@@ -68,7 +68,9 @@ class FlockMCPClientManager:
|
|
|
68
68
|
self._pool: dict[tuple[str, str], dict[str, FlockMCPClient]] = {}
|
|
69
69
|
self._lock = asyncio.Lock()
|
|
70
70
|
|
|
71
|
-
async def get_client(
|
|
71
|
+
async def get_client(
|
|
72
|
+
self, server_name: str, agent_id: str, run_id: str, mount_points: list[str] | None = None
|
|
73
|
+
) -> FlockMCPClient:
|
|
72
74
|
"""Get or create an MCP client for the given context.
|
|
73
75
|
|
|
74
76
|
Architecture Decision: AD005 - Lazy Connection Establishment
|
|
@@ -105,6 +107,24 @@ class FlockMCPClientManager:
|
|
|
105
107
|
f"(agent={agent_id}, run={run_id})"
|
|
106
108
|
)
|
|
107
109
|
config = self._configs[server_name]
|
|
110
|
+
# MCP-ROOTS: Override mount points if provided
|
|
111
|
+
if mount_points:
|
|
112
|
+
from flock.mcp.types import MCPRoot
|
|
113
|
+
|
|
114
|
+
# Create MCPRoot objects from paths
|
|
115
|
+
roots = [
|
|
116
|
+
MCPRoot(uri=f"file://{path}", name=path.split("/")[-1])
|
|
117
|
+
for path in mount_points
|
|
118
|
+
]
|
|
119
|
+
logger.info(
|
|
120
|
+
f"Setting {len(roots)} mount point(s) for server '{server_name}' "
|
|
121
|
+
f"(agent={agent_id}, run={run_id}): {[r.uri for r in roots]}"
|
|
122
|
+
)
|
|
123
|
+
# Clone config with new mount points
|
|
124
|
+
from copy import deepcopy
|
|
125
|
+
|
|
126
|
+
config = deepcopy(config)
|
|
127
|
+
config.connection_config.mount_points = roots
|
|
108
128
|
|
|
109
129
|
# Instantiate the correct concrete client class based on transport type
|
|
110
130
|
# Lazy import to avoid requiring all dependencies
|
|
@@ -145,7 +165,11 @@ class FlockMCPClientManager:
|
|
|
145
165
|
return self._pool[key][server_name]
|
|
146
166
|
|
|
147
167
|
async def get_tools_for_agent(
|
|
148
|
-
self,
|
|
168
|
+
self,
|
|
169
|
+
agent_id: str,
|
|
170
|
+
run_id: str,
|
|
171
|
+
server_names: set[str],
|
|
172
|
+
server_mounts: dict[str, list[str]] | None = None,
|
|
149
173
|
) -> dict[str, Any]:
|
|
150
174
|
"""Get all tools from specified servers for an agent.
|
|
151
175
|
|
|
@@ -156,6 +180,7 @@ class FlockMCPClientManager:
|
|
|
156
180
|
agent_id: Agent requesting tools
|
|
157
181
|
run_id: Current run identifier
|
|
158
182
|
server_names: Set of MCP server names to fetch tools from
|
|
183
|
+
server_mounts: Optional dict mapping server names to mount points
|
|
159
184
|
|
|
160
185
|
Returns:
|
|
161
186
|
Dictionary mapping namespaced tool names to tool definitions
|
|
@@ -166,10 +191,16 @@ class FlockMCPClientManager:
|
|
|
166
191
|
other servers rather than failing the entire operation.
|
|
167
192
|
"""
|
|
168
193
|
tools = {}
|
|
194
|
+
server_mounts = server_mounts or {}
|
|
169
195
|
|
|
170
196
|
for server_name in server_names:
|
|
171
197
|
try:
|
|
172
|
-
|
|
198
|
+
# Get mount points specific to this server
|
|
199
|
+
mount_points = server_mounts.get(server_name)
|
|
200
|
+
|
|
201
|
+
client = await self.get_client(
|
|
202
|
+
server_name, agent_id, run_id, mount_points=mount_points
|
|
203
|
+
)
|
|
173
204
|
server_tools = await client.get_tools(agent_id, run_id)
|
|
174
205
|
|
|
175
206
|
# Apply namespacing: AD003
|
flock/mcp/types/callbacks.py
CHANGED
|
@@ -64,7 +64,10 @@ async def default_list_roots_callback(
|
|
|
64
64
|
) -> ListRootsResult | ErrorData:
|
|
65
65
|
"""Default List Roots Callback."""
|
|
66
66
|
if associated_client.config.feature_config.roots_enabled:
|
|
67
|
-
|
|
67
|
+
# Use lock-free version to avoid deadlock during initialization
|
|
68
|
+
# when the lock is already held by _connect()
|
|
69
|
+
current_roots = associated_client._get_roots_no_lock()
|
|
70
|
+
logger.debug(f"Server requested list/roots. Sending: {current_roots}")
|
|
68
71
|
return ListRootsResult(roots=current_roots)
|
|
69
72
|
return ErrorData(code=INVALID_REQUEST, message="List roots not supported.")
|
|
70
73
|
|
flock/orchestrator.py
CHANGED
|
@@ -7,7 +7,7 @@ import os
|
|
|
7
7
|
from asyncio import Task
|
|
8
8
|
from collections.abc import Iterable, Mapping, Sequence
|
|
9
9
|
from contextlib import asynccontextmanager
|
|
10
|
-
from typing import TYPE_CHECKING, Any
|
|
10
|
+
from typing import TYPE_CHECKING, Any, AsyncGenerator
|
|
11
11
|
from uuid import uuid4
|
|
12
12
|
|
|
13
13
|
from opentelemetry import trace
|
|
@@ -193,8 +193,8 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
193
193
|
enable_prompts_feature: bool = True,
|
|
194
194
|
enable_sampling_feature: bool = True,
|
|
195
195
|
enable_roots_feature: bool = True,
|
|
196
|
+
mount_points: list[str] | None = None,
|
|
196
197
|
tool_whitelist: list[str] | None = None,
|
|
197
|
-
allow_all_tools: bool = True,
|
|
198
198
|
read_timeout_seconds: float = 300,
|
|
199
199
|
max_retries: int = 3,
|
|
200
200
|
**kwargs,
|
|
@@ -212,7 +212,6 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
212
212
|
enable_sampling_feature: Enable LLM sampling requests
|
|
213
213
|
enable_roots_feature: Enable filesystem roots
|
|
214
214
|
tool_whitelist: Optional list of tool names to allow
|
|
215
|
-
allow_all_tools: If True, allow all tools (subject to whitelist)
|
|
216
215
|
read_timeout_seconds: Timeout for server communications
|
|
217
216
|
max_retries: Connection retry attempts
|
|
218
217
|
|
|
@@ -244,12 +243,43 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
244
243
|
else:
|
|
245
244
|
transport_type = "custom"
|
|
246
245
|
|
|
246
|
+
mcp_roots = None
|
|
247
|
+
if mount_points:
|
|
248
|
+
from pathlib import Path as PathLib
|
|
249
|
+
|
|
250
|
+
from flock.mcp.types import MCPRoot
|
|
251
|
+
|
|
252
|
+
mcp_roots = []
|
|
253
|
+
for path in mount_points:
|
|
254
|
+
# Normalize the path
|
|
255
|
+
if path.startswith("file://"):
|
|
256
|
+
# Already a file URI
|
|
257
|
+
uri = path
|
|
258
|
+
# Extract path from URI for name
|
|
259
|
+
path_str = path.replace("file://", "")
|
|
260
|
+
# the test:// path-prefix is used by testing servers such as the mcp-everything server.
|
|
261
|
+
elif path.startswith("test://"):
|
|
262
|
+
# Already a test URI
|
|
263
|
+
uri = path
|
|
264
|
+
# Extract path from URI for name
|
|
265
|
+
path_str = path.replace("test://", "")
|
|
266
|
+
else:
|
|
267
|
+
# Convert to absolute path and create URI
|
|
268
|
+
abs_path = PathLib(path).resolve()
|
|
269
|
+
uri = f"file://{abs_path}"
|
|
270
|
+
path_str = str(abs_path)
|
|
271
|
+
|
|
272
|
+
# Extract a meaningful name (last component of path)
|
|
273
|
+
name = PathLib(path_str).name or path_str.rstrip("/").split("/")[-1] or "root"
|
|
274
|
+
mcp_roots.append(MCPRoot(uri=uri, name=name))
|
|
275
|
+
|
|
247
276
|
# Build configuration
|
|
248
277
|
connection_config = FlockMCPConnectionConfiguration(
|
|
249
278
|
max_retries=max_retries,
|
|
250
279
|
connection_parameters=connection_params,
|
|
251
280
|
transport_type=transport_type,
|
|
252
281
|
read_timeout_seconds=read_timeout_seconds,
|
|
282
|
+
mount_points=mcp_roots,
|
|
253
283
|
)
|
|
254
284
|
|
|
255
285
|
feature_config = FlockMCPFeatureConfiguration(
|
|
@@ -262,7 +292,6 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
262
292
|
|
|
263
293
|
mcp_config = FlockMCPConfiguration(
|
|
264
294
|
name=name,
|
|
265
|
-
allow_all_tools=allow_all_tools,
|
|
266
295
|
connection_config=connection_config,
|
|
267
296
|
feature_config=feature_config,
|
|
268
297
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flock-core
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.0b65
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Author-email: Andre Ratzenberger <andre.ratzenberger@whiteduck.de>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -9,6 +9,7 @@ Requires-Dist: devtools>=0.12.2
|
|
|
9
9
|
Requires-Dist: dspy==3.0.0
|
|
10
10
|
Requires-Dist: duckdb>=1.1.0
|
|
11
11
|
Requires-Dist: fastapi>=0.117.1
|
|
12
|
+
Requires-Dist: hanging-threads>=2.0.7
|
|
12
13
|
Requires-Dist: httpx>=0.28.1
|
|
13
14
|
Requires-Dist: litellm==1.75.3
|
|
14
15
|
Requires-Dist: loguru>=0.7.3
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
flock/__init__.py,sha256=fvp4ltfaAGmYliShuTY_XVIpOUN6bMXbWiBnwb1NBoM,310
|
|
2
|
-
flock/agent.py,sha256=
|
|
2
|
+
flock/agent.py,sha256=LHc2COiV_w0eKmGaq9IsTDaNg_gc6iTSKmGPk1FKbt0,39731
|
|
3
3
|
flock/artifacts.py,sha256=Xnizu0V4Jbwd1yV_FhXjUZG8Em-5GYsLt7sMX0V2OKI,2222
|
|
4
4
|
flock/cli.py,sha256=Ay_Z4KDd5wnnF1fLr0NqjMF7kRm_9X_1RXyv5GETDAY,2126
|
|
5
5
|
flock/components.py,sha256=17vhNMHKc3VUruEbSdb9YNKcDziIe0coS9jpfWBmX4o,6259
|
|
6
6
|
flock/examples.py,sha256=61xkD48yCW-aDrUXIrqrvxLtV8Vn4DrraQrHrrEUnWA,3530
|
|
7
|
-
flock/orchestrator.py,sha256=
|
|
7
|
+
flock/orchestrator.py,sha256=QUFOrgOdMr_TofJnudXmlM6eYYafinrCcFUpKiV2gK4,34963
|
|
8
8
|
flock/registry.py,sha256=s0-H-TMtOsDZiZQCc7T1tYiWQg3OZHn5T--jaI_INIc,4786
|
|
9
9
|
flock/runtime.py,sha256=UG-38u578h628mSddBmyZn2VIzFQ0wlHCpCALFiScqA,8518
|
|
10
10
|
flock/service.py,sha256=cfvcsqZw9xOsEggncZ08zmiDxFiZpxsBYDSji5OIQQ8,5256
|
|
@@ -18,12 +18,12 @@ flock/dashboard/collector.py,sha256=dF8uddDMpOSdxGkhDSAvRNNaABo-TfOceipf1SQmLSU,
|
|
|
18
18
|
flock/dashboard/events.py,sha256=ujdmRJK-GQubrv43qfQ73dnrTj7g39VzBkWfmskJ0j8,5234
|
|
19
19
|
flock/dashboard/launcher.py,sha256=zXWVpyLNxCIu6fJ2L2j2sJ4oDWTvkxhT4FWz7K6eooM,8122
|
|
20
20
|
flock/dashboard/service.py,sha256=30_uUxDhvX1jfy02G4muNuzbcMH5TujY_G5aXWNQa8Q,32104
|
|
21
|
-
flock/dashboard/websocket.py,sha256=
|
|
21
|
+
flock/dashboard/websocket.py,sha256=6piBJ92nSAeTgGFDUgAaQ8CthqThwRUC6aMUBB_2O6E,9508
|
|
22
22
|
flock/engines/__init__.py,sha256=waNyObJ8PKCLFZL3WUFynxSK-V47m559P3Px-vl_OSc,124
|
|
23
|
-
flock/engines/dspy_engine.py,sha256=
|
|
23
|
+
flock/engines/dspy_engine.py,sha256=mEhOKPsTnKN2BpnBv_cntqxH7hhg0Fv_QQuQneyDHPk,36176
|
|
24
24
|
flock/frontend/README.md,sha256=OFdOItV8FGifmUDb694rV2xLC0vl1HlR5KBEtYv5AB0,25054
|
|
25
25
|
flock/frontend/index.html,sha256=BFg1VR_YVAJ_MGN16xa7sT6wTGwtFYUhfJhGuKv89VM,312
|
|
26
|
-
flock/frontend/package-lock.json,sha256=
|
|
26
|
+
flock/frontend/package-lock.json,sha256=PKQaw0nU0PRUWiJ3OIQyS9v-vphokMbUkxoPdgdkiZo,150798
|
|
27
27
|
flock/frontend/package.json,sha256=8cpupeJo0xlJgHiEHpBYCU-nHFiiNGJ73NfZLr5-FSg,1258
|
|
28
28
|
flock/frontend/tsconfig.json,sha256=B9p9jXohg_jrCZAq5_yIHvznpeXHiHQkwUZrVE2oMRA,705
|
|
29
29
|
flock/frontend/tsconfig.node.json,sha256=u5_YWSqeNkZBRBIZ8Q2E2q6bospcyF23mO-taRO7glc,233
|
|
@@ -152,9 +152,9 @@ flock/logging/telemetry_exporter/duckdb_exporter.py,sha256=MoFFsAb-twZF9ZAB0s8TO
|
|
|
152
152
|
flock/logging/telemetry_exporter/file_exporter.py,sha256=uTgQZSKfbLjzypirBbiVnccaKBDkiRzhFvUdV26pKEY,3126
|
|
153
153
|
flock/logging/telemetry_exporter/sqlite_exporter.py,sha256=754G8s4X-JHHpCdoNydJnPX4NZijZk9URutfPWFIT7s,3479
|
|
154
154
|
flock/mcp/__init__.py,sha256=QuANhTz09E2fFhtp2yJ716WxXKwEpCMTGqHj54VRs2Q,2512
|
|
155
|
-
flock/mcp/client.py,sha256=
|
|
156
|
-
flock/mcp/config.py,sha256=
|
|
157
|
-
flock/mcp/manager.py,sha256=
|
|
155
|
+
flock/mcp/client.py,sha256=FRkuWNohFhzAzv-8aLz8Nd3CxkCf_SgHpfbPGTjRbMg,25790
|
|
156
|
+
flock/mcp/config.py,sha256=EgCFYTkV97fy8PlmnFVkB8FLCCSIni5vF3mQB0Yy-3c,14850
|
|
157
|
+
flock/mcp/manager.py,sha256=X54Ph5CtcIIZNuWndSH2sZijVjVBpPCNHS4M-B1bViU,10942
|
|
158
158
|
flock/mcp/tool.py,sha256=cn59dFM1wD0_hl0GjcHxtVYEmPClqskYVMc9AVMB9RY,5397
|
|
159
159
|
flock/mcp/servers/sse/__init__.py,sha256=qyfDdkZkgscHOkFREvsvoNtzA-CCnXPNma5w9uaIUCo,51
|
|
160
160
|
flock/mcp/servers/sse/flock_sse_server.py,sha256=dbkRAcn11V9bBEoPlCMuuzuErTjHigqsnvGYJL4v2ts,3685
|
|
@@ -165,7 +165,7 @@ flock/mcp/servers/streamable_http/flock_streamable_http_server.py,sha256=qPAypNi
|
|
|
165
165
|
flock/mcp/servers/websockets/__init__.py,sha256=KeNgNQRdeCQ9xgpaHB1I0-HyYeBhkifAuZPTIA8eDqM,47
|
|
166
166
|
flock/mcp/servers/websockets/flock_websocket_server.py,sha256=UywSLftw3stbg_b-3Ygn_tNMo5Btm6I3bmSebcprfjk,3118
|
|
167
167
|
flock/mcp/types/__init__.py,sha256=iwO7MXRjA-WhIyHSCI9fR6KF_UhzjqXJWT13f_8w4HM,1274
|
|
168
|
-
flock/mcp/types/callbacks.py,sha256=
|
|
168
|
+
flock/mcp/types/callbacks.py,sha256=8S-ZhWTnR7dzUtxTc9nFcKCDbNyhnE3Bsm1QpA_ArEg,2607
|
|
169
169
|
flock/mcp/types/factories.py,sha256=B7h8apMDT2nR6POOj7aWWn39UbELt6nxHluPhRmemGw,3424
|
|
170
170
|
flock/mcp/types/handlers.py,sha256=6ukkSMv1VZSfk2QDUiJnm8xifHnQvWZsxWXqN21BYSg,7804
|
|
171
171
|
flock/mcp/types/types.py,sha256=ZbzbVihABFnfmZz2X-CCN7hQDzaSY0T-en43PFbFwQQ,11469
|
|
@@ -508,8 +508,8 @@ flock/themes/zenburned.toml,sha256=UEmquBbcAO3Zj652XKUwCsNoC2iQSlIh-q5c6DH-7Kc,1
|
|
|
508
508
|
flock/themes/zenwritten-dark.toml,sha256=-dgaUfg1iCr5Dv4UEeHv_cN4GrPUCWAiHSxWK20X1kI,1663
|
|
509
509
|
flock/themes/zenwritten-light.toml,sha256=G1iEheCPfBNsMTGaVpEVpDzYBHA_T-MV27rolUYolmE,1666
|
|
510
510
|
flock/utility/output_utility_component.py,sha256=yVHhlIIIoYKziI5UyT_zvQb4G-NsxCTgLwA1wXXTTj4,9047
|
|
511
|
-
flock_core-0.5.
|
|
512
|
-
flock_core-0.5.
|
|
513
|
-
flock_core-0.5.
|
|
514
|
-
flock_core-0.5.
|
|
515
|
-
flock_core-0.5.
|
|
511
|
+
flock_core-0.5.0b65.dist-info/METADATA,sha256=foWf00WbMuVzmlasEjY3lQxyrju5nmdGQO2iFa5FHf0,34987
|
|
512
|
+
flock_core-0.5.0b65.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
513
|
+
flock_core-0.5.0b65.dist-info/entry_points.txt,sha256=UQdPmtHd97gSA_IdLt9MOd-1rrf_WO-qsQeIiHWVrp4,42
|
|
514
|
+
flock_core-0.5.0b65.dist-info/licenses/LICENSE,sha256=U3IZuTbC0yLj7huwJdldLBipSOHF4cPf6cUOodFiaBE,1072
|
|
515
|
+
flock_core-0.5.0b65.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|