forked from lastmile-ai/mcp-agent
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfig.py
More file actions
512 lines (368 loc) · 16 KB
/
Copy pathconfig.py
File metadata and controls
512 lines (368 loc) · 16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
"""
Reading settings from environment variables and providing a settings object
for the application configuration.
"""
from pathlib import Path
from typing import Dict, List, Literal, Optional
from pydantic import BaseModel, ConfigDict, Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class MCPServerAuthSettings(BaseModel):
"""Represents authentication configuration for a server."""
api_key: str | None = None
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
class MCPRootSettings(BaseModel):
"""Represents a root directory configuration for an MCP server."""
uri: str
"""The URI identifying the root. Must start with file://"""
name: Optional[str] = None
"""Optional name for the root."""
server_uri_alias: Optional[str] = None
"""Optional URI alias for presentation to the server"""
@field_validator("uri", "server_uri_alias")
@classmethod
def validate_uri(cls, v: str) -> str:
"""Validate that the URI starts with file:// (required by specification 2024-11-05)"""
if not v.startswith("file://"):
raise ValueError("Root URI must start with file://")
return v
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
class MCPServerSettings(BaseModel):
"""
Represents the configuration for an individual server.
"""
# TODO: saqadri - server name should be something a server can provide itself during initialization
name: str | None = None
"""The name of the server."""
# TODO: saqadri - server description should be something a server can provide itself during initialization
description: str | None = None
"""The description of the server."""
transport: Literal["stdio", "sse", "streamable_http", "websocket"] = "stdio"
"""The transport mechanism."""
command: str | None = None
"""The command to execute the server (e.g. npx) in stdio mode."""
args: List[str] = Field(default_factory=list)
"""The arguments for the server command in stdio mode."""
url: str | None = None
"""The URL for the server for SSE, Streamble HTTP or websocket transport."""
headers: Dict[str, str] | None = None
"""HTTP headers for SSE or Streamable HTTP requests."""
http_timeout_seconds: int | None = None
"""
HTTP request timeout in seconds for SSE or Streamable HTTP requests.
Note: This is different from read_timeout_seconds, which
determines how long (in seconds) the client will wait for a new
event before disconnecting
"""
read_timeout_seconds: int | None = None
"""
Timeout in seconds the client will wait for a new event before
disconnecting from an SSE or Streamable HTTP server connection.
"""
terminate_on_close: bool = True
"""
For Streamable HTTP transport, whether to terminate the session on connection close.
"""
auth: MCPServerAuthSettings | None = None
"""The authentication configuration for the server."""
roots: List[MCPRootSettings] | None = None
"""Root directories this server has access to."""
env: Dict[str, str] | None = None
"""Environment variables to pass to the server process."""
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
class MCPSettings(BaseModel):
"""Configuration for all MCP servers."""
servers: Dict[str, MCPServerSettings] = Field(default_factory=dict)
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
class VertexAISettings(BaseModel):
"""Settings for using VertexAI models in the MCP Agent application"""
project: str | None = None
location: str | None = None
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
class BedrockSettings(BaseModel):
"""
Settings for using Bedrock models in the MCP Agent application.
"""
aws_access_key_id: str | None = None
aws_secret_access_key: str | None = None
aws_session_token: str | None = None
aws_region: str | None = None
profile: str | None = None
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
class AnthropicSettings(VertexAISettings, BedrockSettings):
"""
Settings for using Anthropic models in the MCP Agent application.
"""
api_key: str | None = None
default_model: str | None = None
provider: Literal["anthropic", "bedrock", "vertexai"] = "anthropic"
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
class CohereSettings(BaseModel):
"""
Settings for using Cohere models in the MCP Agent application.
"""
api_key: str | None = None
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
class OpenAISettings(BaseModel):
"""
Settings for using OpenAI models in the MCP Agent application.
"""
api_key: str | None = None
reasoning_effort: Literal["low", "medium", "high"] = "medium"
base_url: str | None = None
user: str | None = None
default_headers: Dict[str, str] | None = None
default_model: str | None = None
# NOTE: An http_client can be programmatically specified
# and will be used by the OpenAI client. However, since it is
# not a JSON-serializable object, it cannot be set via configuration.
# http_client: Client | None = None
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
class AzureSettings(BaseModel):
"""
Settings for using Azure models in the MCP Agent application.
"""
api_key: str | None = None
endpoint: str
credential_scopes: List[str] | None = Field(
default=["https://cognitiveservices.azure.com/.default"]
)
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
class GoogleSettings(BaseModel):
"""
Settings for using Google models in the MCP Agent application.
"""
api_key: str | None = None
"""Or use the GOOGLE_API_KEY environment variable"""
vertexai: bool = False
project: str | None = None
location: str | None = None
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
class TemporalSettings(BaseModel):
"""
Temporal settings for the MCP Agent application.
"""
host: str
namespace: str = "default"
api_key: str | None = None
tls: bool = False
task_queue: str
max_concurrent_activities: int | None = None
timeout_seconds: int | None = 60
class UsageTelemetrySettings(BaseModel):
"""
Settings for usage telemetry in the MCP Agent application.
Anonymized usage metrics are sent to a telemetry server to help improve the product.
"""
enabled: bool = True
"""Enable usage telemetry in the MCP Agent application."""
enable_detailed_telemetry: bool = False
"""If enabled, detailed telemetry data, including prompts and agents, will be sent to the telemetry server."""
class TracePathSettings(BaseModel):
"""
Settings for configuring trace file paths with dynamic elements like timestamps or session IDs.
"""
path_pattern: str = "traces/mcp-agent-trace-{unique_id}.jsonl"
"""
Path pattern for trace files with a {unique_id} placeholder.
The placeholder will be replaced according to the unique_id setting.
Example: "traces/mcp-agent-trace-{unique_id}.jsonl"
"""
unique_id: Literal["timestamp", "session_id"] = "timestamp"
"""
Type of unique identifier to use in the trace filename:
"""
timestamp_format: str = "%Y%m%d_%H%M%S"
"""
Format string for timestamps when unique_id is set to "timestamp".
Uses Python's datetime.strftime format.
"""
class TraceOTLPSettings(BaseModel):
"""
Settings for OTLP exporter in OpenTelemetry.
"""
endpoint: str
"""OTLP endpoint for exporting traces."""
class OpenTelemetrySettings(BaseModel):
"""
OTEL settings for the MCP Agent application.
"""
enabled: bool = False
exporters: List[Literal["console", "file", "otlp"]] = []
"""List of exporters to use (can enable multiple simultaneously)"""
service_name: str = "mcp-agent"
service_instance_id: str | None = None
service_version: str | None = None
sample_rate: float = 1.0
"""Sample rate for tracing (1.0 = sample everything)"""
otlp_settings: TraceOTLPSettings | None = None
"""OTLP settings for OpenTelemetry tracing. Required if using otlp exporter."""
# Settings for advanced trace path configuration for file exporter
path_settings: TracePathSettings | None = None
"""
Save trace files with more advanced path semantics, like having timestamps or session id in the trace name.
"""
class LogPathSettings(BaseModel):
"""
Settings for configuring log file paths with dynamic elements like timestamps or session IDs.
"""
path_pattern: str = "logs/mcp-agent-{unique_id}.jsonl"
"""
Path pattern for log files with a {unique_id} placeholder.
The placeholder will be replaced according to the unique_id setting.
Example: "logs/mcp-agent-{unique_id}.jsonl"
"""
unique_id: Literal["timestamp", "session_id"] = "timestamp"
"""
Type of unique identifier to use in the log filename:
- timestamp: Uses the current time formatted according to timestamp_format
- session_id: Generates a UUID for the session
"""
timestamp_format: str = "%Y%m%d_%H%M%S"
"""
Format string for timestamps when unique_id is set to "timestamp".
Uses Python's datetime.strftime format.
"""
class LoggerSettings(BaseModel):
"""
Logger settings for the MCP Agent application.
"""
# Original transport configuration (kept for backward compatibility)
type: Literal["none", "console", "file", "http"] = "console"
transports: List[Literal["none", "console", "file", "http"]] = []
"""List of transports to use (can enable multiple simultaneously)"""
level: Literal["debug", "info", "warning", "error"] = "info"
"""Minimum logging level"""
progress_display: bool = False
"""Enable or disable the progress display"""
path: str = "mcp-agent.jsonl"
"""Path to log file, if logger 'type' is 'file'."""
# Settings for advanced log path configuration
path_settings: LogPathSettings | None = None
"""
Save log files with more advanced path semantics, like having timestamps or session id in the log name.
"""
batch_size: int = 100
"""Number of events to accumulate before processing"""
flush_interval: float = 2.0
"""How often to flush events in seconds"""
max_queue_size: int = 2048
"""Maximum queue size for event processing"""
# HTTP transport settings
http_endpoint: str | None = None
"""HTTP endpoint for event transport"""
http_headers: dict[str, str] | None = None
"""HTTP headers for event transport"""
http_timeout: float = 5.0
"""HTTP timeout seconds for event transport"""
class Settings(BaseSettings):
"""
Settings class for the MCP Agent application.
"""
model_config = SettingsConfigDict(
env_nested_delimiter="__",
env_file=".env",
env_file_encoding="utf-8",
extra="allow",
nested_model_default_partial_update=True,
) # Customize the behavior of settings here
mcp: MCPSettings | None = MCPSettings()
"""MCP config, such as MCP servers"""
execution_engine: Literal["asyncio", "temporal"] = "asyncio"
"""Execution engine for the MCP Agent application"""
temporal: TemporalSettings | None = None
"""Settings for Temporal workflow orchestration"""
anthropic: AnthropicSettings | None = None
"""Settings for using Anthropic models in the MCP Agent application"""
bedrock: BedrockSettings | None = None
"""Settings for using Bedrock models in the MCP Agent application"""
cohere: CohereSettings | None = None
"""Settings for using Cohere models in the MCP Agent application"""
openai: OpenAISettings | None = None
"""Settings for using OpenAI models in the MCP Agent application"""
azure: AzureSettings | None = None
"""Settings for using Azure models in the MCP Agent application"""
google: GoogleSettings | None = None
"""Settings for using Google models in the MCP Agent application"""
otel: OpenTelemetrySettings | None = OpenTelemetrySettings()
"""OpenTelemetry logging settings for the MCP Agent application"""
logger: LoggerSettings | None = LoggerSettings()
"""Logger settings for the MCP Agent application"""
usage_telemetry: UsageTelemetrySettings | None = UsageTelemetrySettings()
"""Usage tracking settings for the MCP Agent application"""
@classmethod
def find_config(cls) -> Path | None:
"""Find the config file in the current directory or parent directories."""
return cls._find_config(["mcp-agent.config.yaml", "mcp_agent.config.yaml"])
@classmethod
def find_secrets(cls) -> Path | None:
"""Find the secrets file in the current directory or parent directories."""
return cls._find_config(["mcp-agent.secrets.yaml", "mcp_agent.secrets.yaml"])
@classmethod
def _find_config(cls, filenames: List[str]) -> Path | None:
"""Find the config file of one of the possible names in the current directory or parent directories."""
current_dir = Path.cwd()
# Check current directory and parent directories
while current_dir != current_dir.parent:
for filename in filenames:
config_path = current_dir / filename
if config_path.exists():
return config_path
current_dir = current_dir.parent
return None
# Global settings object
_settings: Settings | None = None
def get_settings(config_path: str | None = None) -> Settings:
"""Get settings instance, automatically loading from config file if available."""
def deep_merge(base: dict, update: dict) -> dict:
"""Recursively merge two dictionaries, preserving nested structures."""
merged = base.copy()
for key, value in update.items():
if (
key in merged
and isinstance(merged[key], dict)
and isinstance(value, dict)
):
merged[key] = deep_merge(merged[key], value)
else:
merged[key] = value
return merged
global _settings
if _settings:
return _settings
import yaml # pylint: disable=C0415
merged_settings = {}
# Determine the config file to use
if config_path:
config_file = Path(config_path)
if not config_file.exists():
raise FileNotFoundError(f"Config file not found: {config_path}")
else:
config_file = Settings.find_config()
# If we found a config file, load it
if config_file and config_file.exists():
with open(config_file, "r", encoding="utf-8") as f:
yaml_settings = yaml.safe_load(f) or {}
merged_settings = yaml_settings
# Try to find secrets in the same directory as the config file
config_dir = config_file.parent
secrets_found = False
for secrets_filename in ["mcp-agent.secrets.yaml", "mcp_agent.secrets.yaml"]:
secrets_file = config_dir / secrets_filename
if secrets_file.exists():
with open(secrets_file, "r", encoding="utf-8") as f:
yaml_secrets = yaml.safe_load(f) or {}
merged_settings = deep_merge(merged_settings, yaml_secrets)
secrets_found = True
break
# If no secrets were found in the config directory, fall back to discovery
if not secrets_found:
secrets_file = Settings.find_secrets()
if secrets_file and secrets_file.exists():
with open(secrets_file, "r", encoding="utf-8") as f:
yaml_secrets = yaml.safe_load(f) or {}
merged_settings = deep_merge(merged_settings, yaml_secrets)
_settings = Settings(**merged_settings)
return _settings
# No valid config found anywhere
_settings = Settings()
return _settings