gnosys-strata 1.1.4__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.
- gnosys_strata-1.1.4.dist-info/METADATA +140 -0
- gnosys_strata-1.1.4.dist-info/RECORD +28 -0
- gnosys_strata-1.1.4.dist-info/WHEEL +4 -0
- gnosys_strata-1.1.4.dist-info/entry_points.txt +2 -0
- strata/__init__.py +6 -0
- strata/__main__.py +6 -0
- strata/cli.py +364 -0
- strata/config.py +310 -0
- strata/logging_config.py +109 -0
- strata/main.py +6 -0
- strata/mcp_client_manager.py +282 -0
- strata/mcp_proxy/__init__.py +7 -0
- strata/mcp_proxy/auth_provider.py +200 -0
- strata/mcp_proxy/client.py +162 -0
- strata/mcp_proxy/transport/__init__.py +7 -0
- strata/mcp_proxy/transport/base.py +104 -0
- strata/mcp_proxy/transport/http.py +80 -0
- strata/mcp_proxy/transport/stdio.py +69 -0
- strata/server.py +216 -0
- strata/tools.py +714 -0
- strata/treeshell_functions.py +397 -0
- strata/utils/__init__.py +0 -0
- strata/utils/bm25_search.py +181 -0
- strata/utils/catalog.py +82 -0
- strata/utils/dict_utils.py +29 -0
- strata/utils/field_search.py +233 -0
- strata/utils/shared_search.py +202 -0
- strata/utils/tool_integration.py +269 -0
strata/config.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from dataclasses import dataclass, field, asdict
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict, List, Optional, Callable, Any
|
|
6
|
+
|
|
7
|
+
from platformdirs import user_config_dir
|
|
8
|
+
from watchfiles import awatch
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class MCPServerConfig:
|
|
13
|
+
"""Configuration for a single MCP server."""
|
|
14
|
+
|
|
15
|
+
name: str
|
|
16
|
+
type: str = "stdio" # "stdio", "sse", "http"
|
|
17
|
+
command: str = ""
|
|
18
|
+
args: List[str] = field(default_factory=list)
|
|
19
|
+
env: Dict[str, str] = field(default_factory=dict)
|
|
20
|
+
url: Optional[str] = None
|
|
21
|
+
headers: Dict[str, str] = field(default_factory=dict)
|
|
22
|
+
auth: Optional[str] = None # Auth token/key if needed
|
|
23
|
+
enabled: bool = True
|
|
24
|
+
|
|
25
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
26
|
+
"""Convert to dictionary for storage."""
|
|
27
|
+
return asdict(self)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_dict(cls, data: Dict[str, Any]) -> "MCPServerConfig":
|
|
31
|
+
"""Create from dictionary."""
|
|
32
|
+
# Handle legacy format where type might be missing
|
|
33
|
+
type_val = data.get("type", "stdio")
|
|
34
|
+
if "url" in data and not type_val:
|
|
35
|
+
type_val = "sse" # Guess sse if url present
|
|
36
|
+
|
|
37
|
+
return cls(
|
|
38
|
+
name=data["name"],
|
|
39
|
+
type=type_val,
|
|
40
|
+
command=data.get("command", ""),
|
|
41
|
+
args=data.get("args", []),
|
|
42
|
+
env=data.get("env", {}),
|
|
43
|
+
url=data.get("url"),
|
|
44
|
+
headers=data.get("headers", {}),
|
|
45
|
+
auth=data.get("auth"),
|
|
46
|
+
enabled=data.get("enabled", True),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class MCPServerList:
|
|
51
|
+
"""Manage a list of MCP server configurations and sets."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, config_path: Optional[Path] = None, use_mcp_format: bool = True):
|
|
54
|
+
"""Initialize the MCP server list.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
config_path: Path to the configuration file. If None, uses default.
|
|
58
|
+
use_mcp_format: If True, save in MCP format. If False, use legacy format.
|
|
59
|
+
"""
|
|
60
|
+
if config_path is None:
|
|
61
|
+
# Use platformdirs for cross-platform config directory
|
|
62
|
+
config_dir = Path(user_config_dir("strata"))
|
|
63
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
self.config_path = config_dir / "servers.json"
|
|
65
|
+
else:
|
|
66
|
+
self.config_path = Path(config_path)
|
|
67
|
+
|
|
68
|
+
self.servers: Dict[str, MCPServerConfig] = {}
|
|
69
|
+
self.sets: Dict[str, List[str]] = {}
|
|
70
|
+
self.use_mcp_format = use_mcp_format
|
|
71
|
+
self.load()
|
|
72
|
+
|
|
73
|
+
def load(self) -> None:
|
|
74
|
+
"""Load server configurations and sets from file."""
|
|
75
|
+
if self.config_path.exists():
|
|
76
|
+
try:
|
|
77
|
+
with open(self.config_path, "r", encoding="utf-8") as f:
|
|
78
|
+
data = json.load(f)
|
|
79
|
+
|
|
80
|
+
# Check if it's MCP format (has "mcp" key with "servers" inside)
|
|
81
|
+
if "mcp" in data and "servers" in data["mcp"]:
|
|
82
|
+
# Parse MCP format
|
|
83
|
+
for name, config in data["mcp"]["servers"].items():
|
|
84
|
+
# MCP format doesn't have "name" field, add it
|
|
85
|
+
config_dict = {
|
|
86
|
+
"name": name,
|
|
87
|
+
"type": config.get("type", "stdio"),
|
|
88
|
+
"env": config.get("env", {}),
|
|
89
|
+
"enabled": config.get("enabled", True),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Add type-specific fields
|
|
93
|
+
if config.get("type") in ["sse", "http"]:
|
|
94
|
+
config_dict["url"] = config.get("url")
|
|
95
|
+
config_dict["headers"] = config.get("headers", {})
|
|
96
|
+
config_dict["auth"] = config.get("auth", "")
|
|
97
|
+
else: # stdio/command
|
|
98
|
+
config_dict["command"] = config.get("command", "")
|
|
99
|
+
config_dict["args"] = config.get("args", [])
|
|
100
|
+
|
|
101
|
+
self.servers[name] = MCPServerConfig.from_dict(config_dict)
|
|
102
|
+
|
|
103
|
+
# Parse sets if available
|
|
104
|
+
self.sets = data["mcp"].get("sets", {})
|
|
105
|
+
|
|
106
|
+
# Otherwise check for legacy format
|
|
107
|
+
elif "servers" in data:
|
|
108
|
+
# Parse legacy format
|
|
109
|
+
for name, config in data["servers"].items():
|
|
110
|
+
self.servers[name] = MCPServerConfig.from_dict(config)
|
|
111
|
+
# Legacy format doesn't support sets usually, or we could add it top-level
|
|
112
|
+
self.sets = data.get("sets", {})
|
|
113
|
+
except Exception as e:
|
|
114
|
+
print(f"Error loading config from {self.config_path}: {e}")
|
|
115
|
+
|
|
116
|
+
def save(self) -> None:
|
|
117
|
+
"""Save server configurations and sets to file."""
|
|
118
|
+
if self.use_mcp_format:
|
|
119
|
+
# Save in MCP format
|
|
120
|
+
servers_dict = {}
|
|
121
|
+
for name, server in self.servers.items():
|
|
122
|
+
server_config = {}
|
|
123
|
+
|
|
124
|
+
# Add type field if not stdio (default)
|
|
125
|
+
if server.type and server.type != "stdio":
|
|
126
|
+
server_config["type"] = server.type
|
|
127
|
+
|
|
128
|
+
# Add type-specific fields
|
|
129
|
+
if server.type in ["sse", "http"]:
|
|
130
|
+
server_config["url"] = server.url
|
|
131
|
+
if server.headers:
|
|
132
|
+
server_config["headers"] = server.headers
|
|
133
|
+
if server.auth:
|
|
134
|
+
server_config["auth"] = server.auth
|
|
135
|
+
else: # stdio/command
|
|
136
|
+
server_config["command"] = server.command
|
|
137
|
+
server_config["args"] = server.args
|
|
138
|
+
|
|
139
|
+
if server.env:
|
|
140
|
+
server_config["env"] = server.env
|
|
141
|
+
# Always save enabled field to be explicit
|
|
142
|
+
server_config["enabled"] = server.enabled
|
|
143
|
+
servers_dict[name] = server_config
|
|
144
|
+
|
|
145
|
+
data = {
|
|
146
|
+
"mcp": {
|
|
147
|
+
"servers": servers_dict,
|
|
148
|
+
"sets": self.sets
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else:
|
|
152
|
+
# Save in legacy format
|
|
153
|
+
data = {
|
|
154
|
+
"servers": {
|
|
155
|
+
name: server.to_dict() for name, server in self.servers.items()
|
|
156
|
+
},
|
|
157
|
+
"sets": self.sets
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
self.config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
with open(self.config_path, "w", encoding="utf-8") as f:
|
|
162
|
+
json.dump(data, f, indent=2)
|
|
163
|
+
|
|
164
|
+
def add_server(self, server: MCPServerConfig) -> bool:
|
|
165
|
+
"""Add or update a server configuration."""
|
|
166
|
+
if server.name in self.servers and self.servers[server.name] == server:
|
|
167
|
+
return False
|
|
168
|
+
self.servers[server.name] = server
|
|
169
|
+
self.save()
|
|
170
|
+
return True
|
|
171
|
+
|
|
172
|
+
def remove_server(self, name: str) -> bool:
|
|
173
|
+
"""Remove a server configuration."""
|
|
174
|
+
if name in self.servers:
|
|
175
|
+
del self.servers[name]
|
|
176
|
+
# Also remove from any sets
|
|
177
|
+
for set_name in list(self.sets.keys()):
|
|
178
|
+
s = self.sets[set_name]
|
|
179
|
+
if isinstance(s, list) and name in s:
|
|
180
|
+
s.remove(name)
|
|
181
|
+
elif isinstance(s, dict) and name in s.get("servers", []):
|
|
182
|
+
s["servers"].remove(name)
|
|
183
|
+
self.save()
|
|
184
|
+
return True
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
def get_server(self, name: str) -> Optional[MCPServerConfig]:
|
|
188
|
+
"""Get a server configuration by name."""
|
|
189
|
+
return self.servers.get(name)
|
|
190
|
+
|
|
191
|
+
def list_servers(self, enabled_only: bool = False) -> List[MCPServerConfig]:
|
|
192
|
+
"""List all server configurations."""
|
|
193
|
+
servers = list(self.servers.values())
|
|
194
|
+
if enabled_only:
|
|
195
|
+
servers = [s for s in servers if s.enabled]
|
|
196
|
+
return servers
|
|
197
|
+
|
|
198
|
+
def enable_server(self, name: str) -> bool:
|
|
199
|
+
"""Enable a server."""
|
|
200
|
+
if name in self.servers:
|
|
201
|
+
self.servers[name].enabled = True
|
|
202
|
+
self.save()
|
|
203
|
+
return True
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
def disable_server(self, name: str) -> bool:
|
|
207
|
+
"""Disable a server."""
|
|
208
|
+
if name in self.servers:
|
|
209
|
+
self.servers[name].enabled = False
|
|
210
|
+
self.save()
|
|
211
|
+
return True
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
# --- Sets Management ---
|
|
215
|
+
|
|
216
|
+
def add_set(self, name: str, server_names: List[str], description: str = "", include_sets: Optional[List[str]] = None) -> None:
|
|
217
|
+
"""Add or update a set of servers.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
name: Name of the set
|
|
221
|
+
server_names: List of server names in the set
|
|
222
|
+
description: Description of the set's purpose
|
|
223
|
+
include_sets: List of other set names to include (composability)
|
|
224
|
+
"""
|
|
225
|
+
self.sets[name] = {
|
|
226
|
+
"description": description,
|
|
227
|
+
"servers": server_names
|
|
228
|
+
}
|
|
229
|
+
if include_sets:
|
|
230
|
+
self.sets[name]["include_sets"] = include_sets
|
|
231
|
+
self.save()
|
|
232
|
+
|
|
233
|
+
def remove_set(self, name: str) -> bool:
|
|
234
|
+
"""Remove a set."""
|
|
235
|
+
if name in self.sets:
|
|
236
|
+
del self.sets[name]
|
|
237
|
+
self.save()
|
|
238
|
+
return True
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
def get_set(self, name: str, _visited: Optional[set] = None) -> Optional[List[str]]:
|
|
242
|
+
"""Get servers in a set, resolving included sets recursively."""
|
|
243
|
+
if _visited is None:
|
|
244
|
+
_visited = set()
|
|
245
|
+
if name in _visited:
|
|
246
|
+
return [] # Prevent infinite loops
|
|
247
|
+
_visited.add(name)
|
|
248
|
+
|
|
249
|
+
s = self.sets.get(name)
|
|
250
|
+
if s is None:
|
|
251
|
+
return None
|
|
252
|
+
if isinstance(s, list):
|
|
253
|
+
return s
|
|
254
|
+
|
|
255
|
+
servers = list(s.get("servers", []))
|
|
256
|
+
for included in s.get("include_sets", []):
|
|
257
|
+
included_servers = self.get_set(included, _visited)
|
|
258
|
+
if included_servers:
|
|
259
|
+
for srv in included_servers:
|
|
260
|
+
if srv not in servers:
|
|
261
|
+
servers.append(srv)
|
|
262
|
+
return servers
|
|
263
|
+
|
|
264
|
+
def get_set_details(self, name: str) -> Optional[Dict[str, Any]]:
|
|
265
|
+
"""Get full set details including description."""
|
|
266
|
+
details = self.sets.get(name)
|
|
267
|
+
if isinstance(details, list):
|
|
268
|
+
return {"description": "", "servers": details}
|
|
269
|
+
return details
|
|
270
|
+
|
|
271
|
+
def list_sets(self) -> Dict[str, Dict[str, Any]]:
|
|
272
|
+
"""List all sets with details.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Dictionary mapping set name to set details (description, servers)
|
|
276
|
+
"""
|
|
277
|
+
# Normalize output for consistency
|
|
278
|
+
result = {}
|
|
279
|
+
for name, data in self.sets.items():
|
|
280
|
+
if isinstance(data, list):
|
|
281
|
+
result[name] = {"description": "", "servers": data}
|
|
282
|
+
else:
|
|
283
|
+
result[name] = data
|
|
284
|
+
return result
|
|
285
|
+
|
|
286
|
+
# -----------------------
|
|
287
|
+
|
|
288
|
+
async def watch_config(
|
|
289
|
+
self,
|
|
290
|
+
on_changed: Callable[[Dict[str, MCPServerConfig]], None],
|
|
291
|
+
):
|
|
292
|
+
"""Watch configuration file for changes and trigger callback."""
|
|
293
|
+
async for changes in awatch(str(self.config_path.parent)):
|
|
294
|
+
config_changed = False
|
|
295
|
+
for _, path in changes:
|
|
296
|
+
if Path(path) == self.config_path:
|
|
297
|
+
config_changed = True
|
|
298
|
+
break
|
|
299
|
+
|
|
300
|
+
if not config_changed:
|
|
301
|
+
continue
|
|
302
|
+
|
|
303
|
+
self.servers.clear()
|
|
304
|
+
self.sets.clear()
|
|
305
|
+
self.load()
|
|
306
|
+
on_changed(dict(self.servers))
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# Global instance for easy access
|
|
310
|
+
mcp_server_list = MCPServerList()
|
strata/logging_config.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Logging configuration for Strata MCP Router."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from platformdirs import user_cache_dir
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BannerFormatter(logging.Formatter):
|
|
11
|
+
"""Custom formatter for banner messages that shows only the message."""
|
|
12
|
+
|
|
13
|
+
def format(self, record):
|
|
14
|
+
return record.getMessage()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def log_banner() -> None:
|
|
18
|
+
"""Log the Klavis AI colorful banner using a temporary handler."""
|
|
19
|
+
# Create a temporary logger with custom formatter
|
|
20
|
+
banner_logger = logging.getLogger("banner")
|
|
21
|
+
banner_logger.setLevel(logging.INFO)
|
|
22
|
+
|
|
23
|
+
# Create a handler with no formatting for clean output
|
|
24
|
+
console_handler = logging.StreamHandler()
|
|
25
|
+
console_handler.setLevel(logging.INFO)
|
|
26
|
+
console_handler.setFormatter(BannerFormatter())
|
|
27
|
+
|
|
28
|
+
banner_logger.addHandler(console_handler)
|
|
29
|
+
banner_logger.propagate = False # Don't propagate to root logger
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
banner_text = """
|
|
33
|
+
\033[1;31m██╗ ██╗\033[1;33m██╗ \033[1;32m█████╗ \033[1;36m██╗ ██╗\033[1;34m██╗\033[1;35m███████╗ \033[1;91m█████╗ \033[1;93m██╗\033[0m
|
|
34
|
+
\033[1;31m██║ ██╔╝\033[1;33m██║ \033[1;32m██╔══██╗\033[1;36m██║ ██║\033[1;34m██║\033[1;35m██╔════╝ \033[1;91m██╔══██╗\033[1;93m██║\033[0m
|
|
35
|
+
\033[1;31m█████╔╝ \033[1;33m██║ \033[1;32m███████║\033[1;36m██║ ██║\033[1;34m██║\033[1;35m███████╗ \033[1;91m███████║\033[1;93m██║\033[0m
|
|
36
|
+
\033[1;31m██╔═██╗ \033[1;33m██║ \033[1;32m██╔══██║\033[1;36m╚██╗ ██╔╝\033[1;34m██║\033[1;35m╚════██║ \033[1;91m██╔══██║\033[1;93m██║\033[0m
|
|
37
|
+
\033[1;31m██║ ██╗\033[1;33m███████╗\033[1;32m██║ ██║ \033[1;36m╚████╔╝ \033[1;34m██║\033[1;35m███████║ \033[1;91m██║ ██║\033[1;93m██║\033[0m
|
|
38
|
+
\033[1;31m╚═╝ ╚═╝\033[1;33m╚══════╝\033[1;32m╚═╝ ╚═╝ \033[1;36m╚═══╝ \033[1;34m╚═╝\033[1;35m╚══════╝ \033[1;91m╚═╝ ╚═╝\033[1;93m╚═╝\033[0m
|
|
39
|
+
|
|
40
|
+
\033[1;32mEmpowering AI with Seamless Integration\033[0m
|
|
41
|
+
|
|
42
|
+
\033[1;36m═════════════════════════════════════════════════════════════════════════════\033[0m
|
|
43
|
+
\033[1;32m STRATA MCP \033[0m·\033[1;33m One MCP server that use tools reliably at any scale \033[0m
|
|
44
|
+
\033[1;36m═════════════════════════════════════════════════════════════════════════════\033[0m
|
|
45
|
+
\033[1;33m→ Starting MCP Server...\033[0m
|
|
46
|
+
"""
|
|
47
|
+
banner_logger.info(banner_text)
|
|
48
|
+
finally:
|
|
49
|
+
# Clean up the handler
|
|
50
|
+
banner_logger.removeHandler(console_handler)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def setup_logging(log_level: str = "INFO", no_banner: bool = False) -> None:
|
|
54
|
+
"""Configure logging to output to both console and file.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
log_level: The logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
58
|
+
no_banner: Skip displaying the banner on startup
|
|
59
|
+
"""
|
|
60
|
+
# Create cache directory for logs
|
|
61
|
+
cache_dir = Path(user_cache_dir("strata"))
|
|
62
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
|
|
64
|
+
# Generate log file name with timestamp
|
|
65
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
66
|
+
log_file = cache_dir / f"{timestamp}.log"
|
|
67
|
+
|
|
68
|
+
# Configure root logger
|
|
69
|
+
root_logger = logging.getLogger()
|
|
70
|
+
root_logger.setLevel(getattr(logging, log_level.upper()))
|
|
71
|
+
|
|
72
|
+
# Clear any existing handlers
|
|
73
|
+
root_logger.handlers.clear()
|
|
74
|
+
|
|
75
|
+
# Create formatter
|
|
76
|
+
formatter = logging.Formatter(
|
|
77
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
78
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Console handler
|
|
82
|
+
console_handler = logging.StreamHandler()
|
|
83
|
+
console_handler.setLevel(getattr(logging, log_level.upper()))
|
|
84
|
+
console_handler.setFormatter(formatter)
|
|
85
|
+
root_logger.addHandler(console_handler)
|
|
86
|
+
|
|
87
|
+
# Log the setup to console only (before adding file handler)
|
|
88
|
+
logger = logging.getLogger(__name__)
|
|
89
|
+
if not no_banner:
|
|
90
|
+
log_banner()
|
|
91
|
+
# Log initialization message without logging prefix using clean formatter
|
|
92
|
+
logger.info(f"Logging initialized - Console: {log_level}, File: {log_file}")
|
|
93
|
+
|
|
94
|
+
logger.handlers.clear()
|
|
95
|
+
|
|
96
|
+
# File handler (added after the initialization message)
|
|
97
|
+
file_handler = logging.FileHandler(log_file, encoding="utf-8", delay=True)
|
|
98
|
+
file_handler.setLevel(logging.DEBUG) # Always log DEBUG and above to file
|
|
99
|
+
file_handler.setFormatter(formatter)
|
|
100
|
+
root_logger.addHandler(file_handler)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_log_dir() -> Path:
|
|
104
|
+
"""Get the directory where log files are stored.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Path to the log directory
|
|
108
|
+
"""
|
|
109
|
+
return Path(user_cache_dir("strata"))
|