flock-core 0.4.2__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.2.dist-info → flock_core-0.4.5.dist-info}/METADATA +278 -64
- {flock_core-0.4.2.dist-info → flock_core-0.4.5.dist-info}/RECORD +38 -18
- {flock_core-0.4.2.dist-info → flock_core-0.4.5.dist-info}/WHEEL +0 -0
- {flock_core-0.4.2.dist-info → flock_core-0.4.5.dist-info}/entry_points.txt +0 -0
- {flock_core-0.4.2.dist-info → flock_core-0.4.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Manages a pool of connections for a particular server."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from asyncio import Lock
|
|
6
|
+
from typing import Any, Generic, TypeVar
|
|
7
|
+
|
|
8
|
+
from opentelemetry import trace
|
|
9
|
+
from pydantic import (
|
|
10
|
+
BaseModel,
|
|
11
|
+
ConfigDict,
|
|
12
|
+
Field,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from flock.core.logging.logging import get_logger
|
|
16
|
+
from flock.core.mcp.flock_mcp_tool_base import FlockMCPToolBase
|
|
17
|
+
from flock.core.mcp.mcp_client import (
|
|
18
|
+
FlockMCPClientBase,
|
|
19
|
+
)
|
|
20
|
+
from flock.core.mcp.mcp_config import FlockMCPConfigurationBase
|
|
21
|
+
|
|
22
|
+
logger = get_logger("core.mcp.connection_manager_base")
|
|
23
|
+
tracer = trace.get_tracer(__name__)
|
|
24
|
+
|
|
25
|
+
TClient = TypeVar("TClient", bound="FlockMCPClientBase")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FlockMCPClientManagerBase(BaseModel, ABC, Generic[TClient]):
|
|
29
|
+
"""Handles a Pool of MCPClients of type TClient."""
|
|
30
|
+
|
|
31
|
+
client_config: FlockMCPConfigurationBase = Field(
|
|
32
|
+
..., description="Configuration for clients."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
lock: Lock = Field(
|
|
36
|
+
default_factory=Lock,
|
|
37
|
+
description="Lock for mutex access.",
|
|
38
|
+
exclude=True,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
clients: dict[str, dict[str, FlockMCPClientBase]] = Field(
|
|
42
|
+
default_factory=dict,
|
|
43
|
+
exclude=True,
|
|
44
|
+
description="Internal Store for the clients.",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# --- Pydantic v2 Configuratioin ---
|
|
48
|
+
model_config = ConfigDict(
|
|
49
|
+
arbitrary_types_allowed=True,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
async def make_client(
|
|
54
|
+
self,
|
|
55
|
+
additional_params: dict[str, Any] | None = None,
|
|
56
|
+
) -> type[TClient]:
|
|
57
|
+
"""Instantiate-but don't connect yet-a fresh client of the concrete subtype."""
|
|
58
|
+
# default implementation
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
async def get_client(
|
|
62
|
+
self,
|
|
63
|
+
agent_id: str,
|
|
64
|
+
run_id: str,
|
|
65
|
+
additional_params: dict[str, Any] | None = None,
|
|
66
|
+
) -> type[TClient]:
|
|
67
|
+
"""Provides a client from the pool."""
|
|
68
|
+
# Attempt to get a client from the client store.
|
|
69
|
+
# clients are stored like this: agent_id -> run_id -> client
|
|
70
|
+
with tracer.start_as_current_span("client_manager.get_client") as span:
|
|
71
|
+
span.set_attribute("agent_id", agent_id)
|
|
72
|
+
span.set_attribute("run_id", run_id)
|
|
73
|
+
async with self.lock:
|
|
74
|
+
try:
|
|
75
|
+
logger.debug(
|
|
76
|
+
f"Attempting to get client for server '{self.client_config.name}'"
|
|
77
|
+
)
|
|
78
|
+
refresh = False
|
|
79
|
+
if additional_params:
|
|
80
|
+
refresh = bool(
|
|
81
|
+
additional_params.get("refresh_client", False)
|
|
82
|
+
)
|
|
83
|
+
client = None
|
|
84
|
+
run_clients = self.clients.get(agent_id, None)
|
|
85
|
+
if run_clients is None or refresh:
|
|
86
|
+
# This means, that across all runs, no agent has ever needed a client.
|
|
87
|
+
# This also means that we need to create a client.
|
|
88
|
+
client = await self.make_client(
|
|
89
|
+
additional_params=copy.deepcopy(additional_params)
|
|
90
|
+
)
|
|
91
|
+
# Insert the freshly created client
|
|
92
|
+
self.clients[agent_id] = {}
|
|
93
|
+
self.clients[agent_id][run_id] = client
|
|
94
|
+
|
|
95
|
+
else:
|
|
96
|
+
# This means there is at least one entry for the agent_id available
|
|
97
|
+
# Now, all we need to do is check if the run_id matches the entrie's run_id
|
|
98
|
+
client = run_clients.get(run_id, None)
|
|
99
|
+
if client is None or refresh:
|
|
100
|
+
# Means no client here with the respective run_id
|
|
101
|
+
client = await self.make_client(
|
|
102
|
+
additional_params=copy.deepcopy(
|
|
103
|
+
additional_params
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
# Insert the freshly created client.
|
|
107
|
+
self.clients[agent_id][run_id] = client
|
|
108
|
+
|
|
109
|
+
return client
|
|
110
|
+
except Exception as e:
|
|
111
|
+
# Log the exception and raise it so it becomes visible downstream
|
|
112
|
+
logger.error(
|
|
113
|
+
f"Unexpected Exception ocurred while trying to get client for server '{self.client_config.name}' with agent_id: {agent_id} and run_id: {run_id}: {e}"
|
|
114
|
+
)
|
|
115
|
+
span.record_exception(e)
|
|
116
|
+
raise e
|
|
117
|
+
|
|
118
|
+
async def call_tool(
|
|
119
|
+
self,
|
|
120
|
+
agent_id: str,
|
|
121
|
+
run_id: str,
|
|
122
|
+
name: str,
|
|
123
|
+
arguments: dict[str, Any],
|
|
124
|
+
additional_params: dict[str, Any] | None = None,
|
|
125
|
+
) -> Any:
|
|
126
|
+
"""Call a tool."""
|
|
127
|
+
with tracer.start_as_current_span("client_manager.call_tool") as span:
|
|
128
|
+
span.set_attribute("agent_id", agent_id)
|
|
129
|
+
span.set_attribute("run_id", run_id)
|
|
130
|
+
span.set_attribute("tool_name", name)
|
|
131
|
+
span.set_attribute("arguments", str(arguments))
|
|
132
|
+
try:
|
|
133
|
+
client = await self.get_client(
|
|
134
|
+
agent_id=agent_id,
|
|
135
|
+
run_id=run_id,
|
|
136
|
+
additional_params=additional_params,
|
|
137
|
+
)
|
|
138
|
+
result = await client.call_tool(
|
|
139
|
+
agent_id=agent_id,
|
|
140
|
+
run_id=run_id,
|
|
141
|
+
name=name,
|
|
142
|
+
arguments=arguments,
|
|
143
|
+
)
|
|
144
|
+
return result
|
|
145
|
+
except Exception as e:
|
|
146
|
+
logger.error(
|
|
147
|
+
f"Exception occurred while trying to call tool {name} on server '{self.client_config.name}': {e}"
|
|
148
|
+
)
|
|
149
|
+
span.record_exception(e)
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
async def get_tools(
|
|
153
|
+
self,
|
|
154
|
+
agent_id: str,
|
|
155
|
+
run_id: str,
|
|
156
|
+
additional_params: dict[str, Any] | None = None,
|
|
157
|
+
) -> list[FlockMCPToolBase]:
|
|
158
|
+
"""Retrieves a list of tools for the agents to act on."""
|
|
159
|
+
with tracer.start_as_current_span("client_manager.get_tools") as span:
|
|
160
|
+
span.set_attribute("agent_id", agent_id)
|
|
161
|
+
span.set_attribute("run_id", run_id)
|
|
162
|
+
try:
|
|
163
|
+
client = await self.get_client(
|
|
164
|
+
agent_id=agent_id,
|
|
165
|
+
run_id=run_id,
|
|
166
|
+
additional_params=additional_params,
|
|
167
|
+
)
|
|
168
|
+
tools: list[FlockMCPToolBase] = await client.get_tools(
|
|
169
|
+
agent_id=agent_id, run_id=run_id
|
|
170
|
+
)
|
|
171
|
+
return tools
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logger.error(
|
|
174
|
+
f"Exception occurred while trying to retrieve Tools for server '{self.client_config.name}' with agent_id: {agent_id} and run_id: {run_id}: {e}"
|
|
175
|
+
)
|
|
176
|
+
span.record_exception(e)
|
|
177
|
+
return []
|
|
178
|
+
|
|
179
|
+
async def close_all(self) -> None:
|
|
180
|
+
"""Closes all connections in the pool and cancels background tasks."""
|
|
181
|
+
with tracer.start_as_current_span("client_manager.close_all") as span:
|
|
182
|
+
async with self.lock:
|
|
183
|
+
for agent_id, run_dict in self.clients.items():
|
|
184
|
+
logger.debug(
|
|
185
|
+
f"Shutting down all clients for agent_id: {agent_id}"
|
|
186
|
+
)
|
|
187
|
+
for run_id, client in run_dict.items():
|
|
188
|
+
logger.debug(
|
|
189
|
+
f"Shutting down client for agent_id {agent_id} and run_id {run_id}"
|
|
190
|
+
)
|
|
191
|
+
try:
|
|
192
|
+
await client.disconnect()
|
|
193
|
+
except Exception as e:
|
|
194
|
+
logger.error(
|
|
195
|
+
f"Error when trying to disconnect client for server '{self.client_config.name}': {e}"
|
|
196
|
+
)
|
|
197
|
+
span.record_exception(e)
|
|
198
|
+
self.clients = {} # Let the GC take care of the rest.
|
|
199
|
+
logger.info(
|
|
200
|
+
f"All clients disconnected for server '{self.client_config.name}'"
|
|
201
|
+
)
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Base Config for MCP Clients."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal, TypeVar
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field, create_model
|
|
6
|
+
|
|
7
|
+
from flock.core.mcp.types.types import (
|
|
8
|
+
FlockListRootsMCPCallback,
|
|
9
|
+
FlockLoggingMCPCallback,
|
|
10
|
+
FlockMessageHandlerMCPCallback,
|
|
11
|
+
FlockSamplingMCPCallback,
|
|
12
|
+
MCPRoot,
|
|
13
|
+
ServerParameters,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
LoggingLevel = Literal[
|
|
17
|
+
"debug",
|
|
18
|
+
"info",
|
|
19
|
+
"notice",
|
|
20
|
+
"warning",
|
|
21
|
+
"error",
|
|
22
|
+
"critical",
|
|
23
|
+
"alert",
|
|
24
|
+
"emergency",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
A = TypeVar("A", bound="FlockMCPCallbackConfigurationBase")
|
|
29
|
+
B = TypeVar("B", bound="FlockMCPConnectionConfigurationBase")
|
|
30
|
+
C = TypeVar("C", bound="FlockMCPConfigurationBase")
|
|
31
|
+
D = TypeVar("D", bound="FlockMCPCachingConfigurationBase")
|
|
32
|
+
E = TypeVar("E", bound="FlockMCPFeatureConfigurationBase")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FlockMCPCachingConfigurationBase(BaseModel):
|
|
36
|
+
"""Configuration for Caching in Clients."""
|
|
37
|
+
|
|
38
|
+
tool_cache_max_size: float = Field(
|
|
39
|
+
default=100, description="Maximum number of items in the Tool Cache."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
tool_cache_max_ttl: float = Field(
|
|
43
|
+
default=60,
|
|
44
|
+
description="Max TTL for items in the tool cache in seconds.",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
resource_contents_cache_max_size: float = Field(
|
|
48
|
+
default=10,
|
|
49
|
+
description="Maximum number of entries in the Resource Contents cache.",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
resource_contents_cache_max_ttl: float = Field(
|
|
53
|
+
default=60 * 5,
|
|
54
|
+
description="Maximum number of items in the Resource Contents cache.",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
resource_list_cache_max_size: float = Field(
|
|
58
|
+
default=10,
|
|
59
|
+
description="Maximum number of entries in the Resource List Cache.",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
resource_list_cache_max_ttl: float = Field(
|
|
63
|
+
default=100,
|
|
64
|
+
description="Maximum TTL for entries in the Resource List Cache.",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
tool_result_cache_max_size: float = Field(
|
|
68
|
+
default=1000,
|
|
69
|
+
description="Maximum number of entries in the Tool Result Cache.",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
tool_result_cache_max_ttl: float = Field(
|
|
73
|
+
default=20,
|
|
74
|
+
description="Maximum TTL in seconds for entries in the Tool Result Cache.",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
model_config = ConfigDict(
|
|
78
|
+
arbitrary_types_allowed=True,
|
|
79
|
+
extra="allow",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def with_fields(cls: type[D], **field_definitions) -> type[D]:
|
|
84
|
+
"""Create a new config class with additional fields."""
|
|
85
|
+
return create_model(
|
|
86
|
+
f"Dynamic{cls.__name__}", __base__=cls, **field_definitions
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class FlockMCPCallbackConfigurationBase(BaseModel):
|
|
91
|
+
"""Base Configuration Class for Callbacks for Clients."""
|
|
92
|
+
|
|
93
|
+
sampling_callback: FlockSamplingMCPCallback | None = Field(
|
|
94
|
+
default=None,
|
|
95
|
+
description="Callback for handling sampling requests from an external server.",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
list_roots_callback: FlockListRootsMCPCallback | None = Field(
|
|
99
|
+
default=None, description="Callback for handling list roots requests."
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
logging_callback: FlockLoggingMCPCallback | None = Field(
|
|
103
|
+
default=None,
|
|
104
|
+
description="Callback for handling logging messages from an external server.",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
message_handler: FlockMessageHandlerMCPCallback | None = Field(
|
|
108
|
+
default=None,
|
|
109
|
+
description="Callback for handling messages not covered by other callbacks.",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
model_config = ConfigDict(arbitrary_types_allowed=True, extra="allow")
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def with_fields(cls: type[A], **field_definitions) -> type[A]:
|
|
116
|
+
"""Create a new config class with additional fields."""
|
|
117
|
+
return create_model(
|
|
118
|
+
f"Dynamic{cls.__name__}", __base__=cls, **field_definitions
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class FlockMCPConnectionConfigurationBase(BaseModel):
|
|
123
|
+
"""Base Configuration Class for Connection Parameters for a client."""
|
|
124
|
+
|
|
125
|
+
max_retries: int = Field(
|
|
126
|
+
default=3,
|
|
127
|
+
description="How many times to attempt to establish the connection before giving up.",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
connection_parameters: ServerParameters = Field(
|
|
131
|
+
..., description="Connection parameters for the server."
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
transport_type: Literal["stdio", "websockets", "sse", "custom"] = Field(
|
|
135
|
+
..., description="Type of transport to use."
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
mount_points: list[MCPRoot] | None = Field(
|
|
139
|
+
default=None, description="Initial Mountpoints to operate under."
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
read_timeout_seconds: float | int = Field(
|
|
143
|
+
default=60 * 5, description="Read Timeout."
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
server_logging_level: LoggingLevel = Field(
|
|
147
|
+
default="error",
|
|
148
|
+
description="The logging level for logging events from the remote server.",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
model_config = ConfigDict(arbitrary_types_allowed=True, extra="allow")
|
|
152
|
+
|
|
153
|
+
@classmethod
|
|
154
|
+
def with_fields(cls: type[B], **field_definitions) -> type[B]:
|
|
155
|
+
"""Create a new config class with additional fields."""
|
|
156
|
+
return create_model(
|
|
157
|
+
f"Dynamic{cls.__name__}", __base__=cls, **field_definitions
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class FlockMCPFeatureConfigurationBase(BaseModel):
|
|
162
|
+
"""Base Configuration Class for switching MCP Features on and off."""
|
|
163
|
+
|
|
164
|
+
roots_enabled: bool = Field(
|
|
165
|
+
default=False,
|
|
166
|
+
description="Whether or not the Roots feature is enabled for this client.",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
sampling_enabled: bool = Field(
|
|
170
|
+
default=False,
|
|
171
|
+
description="Whether or not the Sampling feature is enabled for this client.",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
tools_enabled: bool = Field(
|
|
175
|
+
default=False,
|
|
176
|
+
description="Whether or not the Tools feature is enabled for this client.",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
prompts_enabled: bool = Field(
|
|
180
|
+
default=False,
|
|
181
|
+
description="Whether or not the Prompts feature is enabled for this client.",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
model_config = ConfigDict(
|
|
185
|
+
arbitrary_types_allowed=True,
|
|
186
|
+
extra="allow",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
@classmethod
|
|
190
|
+
def with_fields(cls: type[E], **field_definitions) -> type[E]:
|
|
191
|
+
"""Create a new config class with additional fields."""
|
|
192
|
+
return create_model(
|
|
193
|
+
f"Dynamic{cls.__name__}", __base__=cls, **field_definitions
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class FlockMCPConfigurationBase(BaseModel):
|
|
198
|
+
"""Base Configuration Class for MCP Clients.
|
|
199
|
+
|
|
200
|
+
Each Client should implement their own config
|
|
201
|
+
model by inheriting from this class.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
name: str = Field(
|
|
205
|
+
..., description="Name of the server the client connects to."
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
connection_config: FlockMCPConnectionConfigurationBase = Field(
|
|
209
|
+
..., description="MCP Connection Configuration for a client."
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
caching_config: FlockMCPCachingConfigurationBase = Field(
|
|
213
|
+
default_factory=FlockMCPCachingConfigurationBase,
|
|
214
|
+
description="Configuration for the internal caches of the client.",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
callback_config: FlockMCPCallbackConfigurationBase = Field(
|
|
218
|
+
default_factory=FlockMCPCallbackConfigurationBase,
|
|
219
|
+
description="Callback configuration for the client.",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
feature_config: FlockMCPFeatureConfigurationBase = Field(
|
|
223
|
+
default_factory=FlockMCPFeatureConfigurationBase,
|
|
224
|
+
description="Feature configuration for the client.",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
model_config = ConfigDict(
|
|
228
|
+
arbitrary_types_allowed=True,
|
|
229
|
+
extra="allow",
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
@classmethod
|
|
233
|
+
def with_fields(cls: type[C], **field_definitions) -> type[C]:
|
|
234
|
+
"""Create a new config class with additional fields."""
|
|
235
|
+
return create_model(
|
|
236
|
+
f"Dynamic{cls.__name__}", __base__=cls, **field_definitions
|
|
237
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MCP Types package."""
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""MCP Callbacks."""
|
|
2
|
+
|
|
3
|
+
from mcp.shared.context import RequestContext
|
|
4
|
+
from mcp.shared.session import RequestResponder
|
|
5
|
+
from mcp.types import (
|
|
6
|
+
INVALID_REQUEST,
|
|
7
|
+
ClientResult,
|
|
8
|
+
CreateMessageRequestParams,
|
|
9
|
+
ErrorData,
|
|
10
|
+
ListRootsResult,
|
|
11
|
+
ServerRequest,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from flock.core.logging.logging import FlockLogger
|
|
15
|
+
from flock.core.mcp.mcp_client import Any
|
|
16
|
+
from flock.core.mcp.types.handlers import (
|
|
17
|
+
handle_incoming_exception,
|
|
18
|
+
handle_incoming_request,
|
|
19
|
+
handle_incoming_server_notification,
|
|
20
|
+
handle_logging_message,
|
|
21
|
+
)
|
|
22
|
+
from flock.core.mcp.types.types import (
|
|
23
|
+
FlockLoggingMessageNotificationParams,
|
|
24
|
+
ServerNotification,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def default_sampling_callback(
|
|
29
|
+
ctx: RequestContext, params: CreateMessageRequestParams, logger: FlockLogger
|
|
30
|
+
) -> ErrorData:
|
|
31
|
+
"""Default Callback for Sampling."""
|
|
32
|
+
logger.info(f"Rejecting Sampling Request.")
|
|
33
|
+
return ErrorData(code=INVALID_REQUEST, message="Sampling not supported.")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def default_message_handler(
|
|
37
|
+
req: RequestResponder[ServerRequest, ClientResult]
|
|
38
|
+
| ServerNotification
|
|
39
|
+
| Exception,
|
|
40
|
+
logger: FlockLogger,
|
|
41
|
+
associated_client: Any,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Default Message Handler."""
|
|
44
|
+
if isinstance(req, Exception):
|
|
45
|
+
await handle_incoming_exception(
|
|
46
|
+
e=req,
|
|
47
|
+
logger_to_use=logger,
|
|
48
|
+
associated_client=associated_client,
|
|
49
|
+
)
|
|
50
|
+
elif isinstance(req, ServerNotification):
|
|
51
|
+
await handle_incoming_server_notification(
|
|
52
|
+
n=req,
|
|
53
|
+
logger=logger,
|
|
54
|
+
client=associated_client,
|
|
55
|
+
)
|
|
56
|
+
elif isinstance(req, RequestResponder[ServerRequest, ClientResult]):
|
|
57
|
+
await handle_incoming_request(
|
|
58
|
+
req=req,
|
|
59
|
+
logger_to_use=logger,
|
|
60
|
+
associated_client=associated_client,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def default_list_roots_callback(
|
|
65
|
+
associated_client: Any,
|
|
66
|
+
logger: FlockLogger,
|
|
67
|
+
) -> ListRootsResult | ErrorData:
|
|
68
|
+
"""Default List Roots Callback."""
|
|
69
|
+
if associated_client.config.feature_config.roots_enabled:
|
|
70
|
+
current_roots = await associated_client.get_current_roots()
|
|
71
|
+
return ListRootsResult(roots=current_roots)
|
|
72
|
+
else:
|
|
73
|
+
return ErrorData(
|
|
74
|
+
code=INVALID_REQUEST, message="List roots not supported."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def default_logging_callback(
|
|
79
|
+
params: FlockLoggingMessageNotificationParams,
|
|
80
|
+
logger: FlockLogger,
|
|
81
|
+
server_name: str,
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Default Logging Handling Callback."""
|
|
84
|
+
await handle_logging_message(
|
|
85
|
+
params=params, logger=logger, server_name=server_name
|
|
86
|
+
)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Factories for default MCP Callbacks."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from mcp.shared.context import RequestContext
|
|
6
|
+
from mcp.types import (
|
|
7
|
+
CreateMessageRequestParams,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from flock.core.logging.logging import FlockLogger, get_logger
|
|
11
|
+
from flock.core.mcp.types.types import (
|
|
12
|
+
FlockListRootsMCPCallback,
|
|
13
|
+
FlockLoggingMCPCallback,
|
|
14
|
+
FlockLoggingMessageNotificationParams,
|
|
15
|
+
FlockMessageHandlerMCPCallback,
|
|
16
|
+
FlockSamplingMCPCallback,
|
|
17
|
+
ServerNotification,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from flock.core.mcp.types.callbacks import (
|
|
22
|
+
default_list_roots_callback,
|
|
23
|
+
default_logging_callback,
|
|
24
|
+
default_message_handler,
|
|
25
|
+
default_sampling_callback,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
default_logging_callback_logger = get_logger("core.mcp.callback.logging")
|
|
29
|
+
default_sampling_callback_logger = get_logger("core.mcp.callback.sampling")
|
|
30
|
+
default_list_roots_callback_logger = get_logger("core.mcp.callback.sampling")
|
|
31
|
+
default_message_handler_logger = get_logger("core.mcp.callback.message")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def default_flock_mcp_logging_callback_factory(
|
|
35
|
+
associated_client: Any,
|
|
36
|
+
logger: FlockLogger | None = None,
|
|
37
|
+
) -> FlockLoggingMCPCallback:
|
|
38
|
+
"""Creates a fallback for handling incoming logging requests."""
|
|
39
|
+
logger_to_use = logger if logger else default_logging_callback_logger
|
|
40
|
+
|
|
41
|
+
async def _method(
|
|
42
|
+
params: FlockLoggingMessageNotificationParams,
|
|
43
|
+
) -> None:
|
|
44
|
+
return await default_logging_callback(
|
|
45
|
+
params=params,
|
|
46
|
+
logger=logger_to_use,
|
|
47
|
+
server_name=associated_client.config.name,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def default_flock_mcp_sampling_callback_factory(
|
|
52
|
+
associated_client: Any,
|
|
53
|
+
logger: FlockLogger | None = None,
|
|
54
|
+
) -> FlockSamplingMCPCallback:
|
|
55
|
+
"""Creates a fallback for handling incoming sampling requests."""
|
|
56
|
+
logger_to_use = logger if logger else default_sampling_callback_logger
|
|
57
|
+
|
|
58
|
+
async def _method(
|
|
59
|
+
ctx: RequestContext,
|
|
60
|
+
params: CreateMessageRequestParams,
|
|
61
|
+
):
|
|
62
|
+
logger_to_use.info(
|
|
63
|
+
f"SAMPLING_REQUEST: server '{associated_client.config.name}' sent a sampling request: {params}"
|
|
64
|
+
)
|
|
65
|
+
await default_sampling_callback(
|
|
66
|
+
ctx=ctx, params=params, logger=logger_to_use
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return _method
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def default_flock_mcp_message_handler_callback_factory(
|
|
73
|
+
associated_client: Any,
|
|
74
|
+
logger: FlockLogger | None = None,
|
|
75
|
+
) -> FlockMessageHandlerMCPCallback:
|
|
76
|
+
"""Creates a fallback for handling incoming messages.
|
|
77
|
+
|
|
78
|
+
Note:
|
|
79
|
+
Incoming Messages differ from incoming requests.
|
|
80
|
+
Requests can do things like list roots, create messages through sampling etc.
|
|
81
|
+
While Incoming Messages mainly consist of miscellanious information
|
|
82
|
+
sent by the server.
|
|
83
|
+
"""
|
|
84
|
+
logger_to_use = logger if logger else default_message_handler_logger
|
|
85
|
+
|
|
86
|
+
async def _method(
|
|
87
|
+
n: ServerNotification,
|
|
88
|
+
) -> None:
|
|
89
|
+
await default_message_handler(
|
|
90
|
+
req=n,
|
|
91
|
+
logger_to_use=logger_to_use,
|
|
92
|
+
associated_client=associated_client,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return _method
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def default_flock_mcp_list_roots_callback_factory(
|
|
99
|
+
associated_client: Any,
|
|
100
|
+
logger: FlockLogger | None = None,
|
|
101
|
+
) -> FlockListRootsMCPCallback:
|
|
102
|
+
"""Creates a fallback for a list roots callback for a client."""
|
|
103
|
+
logger_to_use = logger or default_list_roots_callback_logger
|
|
104
|
+
|
|
105
|
+
async def _method(*args, **kwargs):
|
|
106
|
+
return await default_list_roots_callback(
|
|
107
|
+
associated_client=associated_client,
|
|
108
|
+
logger=logger_to_use,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return _method
|