magg 0.3.1__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.
Files changed (68) hide show
  1. magg/__init__.py +10 -0
  2. magg/__main__.py +7 -0
  3. magg/cli.py +373 -0
  4. magg/discovery/__init__.py +1 -0
  5. magg/discovery/catalog.py +90 -0
  6. magg/discovery/metadata.py +744 -0
  7. magg/discovery/search.py +411 -0
  8. magg/logs/__init__.py +48 -0
  9. magg/logs/adapter.py +18 -0
  10. magg/logs/config.py +48 -0
  11. magg/logs/defaults.py +76 -0
  12. magg/logs/filter.py +17 -0
  13. magg/logs/formatter.py +16 -0
  14. magg/logs/handler.py +35 -0
  15. magg/logs/listener.py +58 -0
  16. magg/logs/queue.py +25 -0
  17. magg/mbro/__init__.py +5 -0
  18. magg/mbro/__main__.py +7 -0
  19. magg/mbro/acon.py +289 -0
  20. magg/mbro/cli.py +679 -0
  21. magg/mbro/client.py +344 -0
  22. magg/mbro/formatter.py +752 -0
  23. magg/mbro/test/__init__.py +1 -0
  24. magg/mbro/test/test_basic_functionality.py +95 -0
  25. magg/mbro/test/test_cli.py +296 -0
  26. magg/mbro/test/test_client.py +54 -0
  27. magg/mbro/test/test_integration.py +128 -0
  28. magg/mbro/test/test_search_functionality.py +171 -0
  29. magg/mbro/test/test_tool_calling.py +132 -0
  30. magg/process.py +48 -0
  31. magg/server/__init__.py +4 -0
  32. magg/server/__main__.py +7 -0
  33. magg/server/cli.py +38 -0
  34. magg/server/defaults.py +14 -0
  35. magg/server/manager.py +181 -0
  36. magg/server/proxy.py +223 -0
  37. magg/server/response.py +201 -0
  38. magg/server/runner.py +171 -0
  39. magg/server/server.py +692 -0
  40. magg/settings.py +247 -0
  41. magg/test/__init__.py +1 -0
  42. magg/test/test_basic.py +102 -0
  43. magg/test/test_client_api.py +135 -0
  44. magg/test/test_client_mounting.py +79 -0
  45. magg/test/test_config.py +213 -0
  46. magg/test/test_config_migration.py +172 -0
  47. magg/test/test_e2e_mounting.py +189 -0
  48. magg/test/test_e2e_simple.py +129 -0
  49. magg/test/test_error_handling.py +179 -0
  50. magg/test/test_integration.py +186 -0
  51. magg/test/test_mounting.py +155 -0
  52. magg/test/test_mounting_debug.py +99 -0
  53. magg/test/test_mounting_real.py +94 -0
  54. magg/test/test_server_add.py +238 -0
  55. magg/test/test_server_removal.py +240 -0
  56. magg/test/test_tool_delegation.py +151 -0
  57. magg/test/test_transport.py +190 -0
  58. magg/util/__init__.py +13 -0
  59. magg/util/system.py +56 -0
  60. magg/util/terminal.py +120 -0
  61. magg/util/transform.py +345 -0
  62. magg/util/transport.py +184 -0
  63. magg/util/transports.py +82 -0
  64. magg/util/uri.py +121 -0
  65. magg-0.3.1.dist-info/METADATA +19 -0
  66. magg-0.3.1.dist-info/RECORD +68 -0
  67. magg-0.3.1.dist-info/WHEEL +4 -0
  68. magg-0.3.1.dist-info/entry_points.txt +4 -0
magg/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """MAGG - MCP Aggregator
2
+
3
+ A self-aware MCP server that manages and aggregates other MCP tools and servers.
4
+ """
5
+ from importlib import metadata
6
+
7
+ try:
8
+ __version__ = metadata.version("magg")
9
+ except metadata.PackageNotFoundError:
10
+ __version__ = "unknown"
magg/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ from magg import process
2
+
3
+ process.setup()
4
+
5
+ from magg.cli import main
6
+
7
+ main()
magg/cli.py ADDED
@@ -0,0 +1,373 @@
1
+ #!/usr/bin/env python3
2
+ """Main CLI interface for MAGG - Simplified implementation."""
3
+
4
+ import argparse
5
+ import asyncio
6
+ import json
7
+ import os
8
+ import sys
9
+ import logging
10
+
11
+ from . import __version__, process
12
+ from .settings import ConfigManager, ServerConfig
13
+ from .util.terminal import (
14
+ print_success, print_error, print_warning,
15
+ print_info, print_server_list, print_status_summary, confirm_action
16
+ )
17
+
18
+
19
+ process.setup(source=__name__)
20
+
21
+ logger: logging.Logger | None = logging.getLogger(__name__)
22
+
23
+
24
+ async def cmd_serve(args) -> None:
25
+ """Start MAGG server."""
26
+ from magg.server.runner import print_startup_banner
27
+ from magg.server.runner import ServerRunner
28
+
29
+ logger.info("Starting MAGG server (mode: %s)", 'http' if args.http else 'stdio')
30
+
31
+ if args.http:
32
+ print_startup_banner()
33
+
34
+ runner = ServerRunner(args.config)
35
+
36
+ if args.http:
37
+ logger.info("Starting HTTP server on %s:%s", args.host, args.port)
38
+ await runner.run_http(host=args.host, port=args.port)
39
+ else:
40
+ logger.info("Starting stdio server")
41
+ await runner.run_stdio()
42
+
43
+
44
+ def cmd_serve_args(parser: argparse.ArgumentParser) -> None:
45
+ parser.add_argument(
46
+ '--http',
47
+ action='store_true',
48
+ help='Run as HTTP server instead of stdio mode'
49
+ )
50
+ parser.add_argument(
51
+ '--host',
52
+ type=str,
53
+ default='localhost',
54
+ help='HTTP server host address (default: localhost)'
55
+ )
56
+ parser.add_argument(
57
+ '--port',
58
+ type=int,
59
+ default=8000,
60
+ help='HTTP server port (default: 8000)'
61
+ )
62
+
63
+
64
+ async def cmd_add_server(args) -> None:
65
+ """Add a new MCP server."""
66
+ config_manager = ConfigManager(args.config)
67
+ config = config_manager.load_config()
68
+
69
+ if args.name in config.servers:
70
+ logger.debug("Attempt to add duplicate server: %s", args.name)
71
+ print_error(f"Server '{args.name}' already exists")
72
+ sys.exit(1)
73
+
74
+ # Parse environment variables
75
+ env = None
76
+ if args.env:
77
+ try:
78
+ env = dict(arg.split('=', 1) for arg in args.env)
79
+ except ValueError:
80
+ print_error("Invalid environment variable format. Use KEY=VALUE")
81
+ sys.exit(1)
82
+
83
+ # Parse command and args
84
+ command = None
85
+ command_args = None
86
+ if args.command:
87
+ parts = args.command.split()
88
+ if parts:
89
+ command = parts[0]
90
+ command_args = parts[1:] if len(parts) > 1 else None
91
+
92
+ try:
93
+ server = ServerConfig(
94
+ name=args.name,
95
+ source=args.source,
96
+ prefix=args.prefix, # Will be auto-generated if not provided
97
+ command=command,
98
+ args=command_args,
99
+ uri=args.uri,
100
+ env=env,
101
+ working_dir=args.working_dir,
102
+ notes=args.notes
103
+ )
104
+ except ValueError as e:
105
+ print_error(f"Invalid server configuration: {e}")
106
+ sys.exit(1)
107
+
108
+ config.add_server(server)
109
+
110
+ if config_manager.save_config(config):
111
+ print_success(f"Added server '{args.name}'")
112
+ print(f" Source: {args.source}")
113
+ print(f" Prefix: {server.prefix}")
114
+ if server.command:
115
+ full_command = server.command
116
+ if server.args:
117
+ full_command += ' ' + ' '.join(server.args)
118
+ print(f" Command: {full_command}")
119
+ if server.notes:
120
+ print(f" Notes: {server.notes}")
121
+ else:
122
+ print_error("Failed to save configuration")
123
+ sys.exit(1)
124
+
125
+
126
+ async def cmd_list_servers(args) -> None:
127
+ """List configured servers."""
128
+ config_manager = ConfigManager(args.config)
129
+ config = config_manager.load_config()
130
+
131
+ # logger.debug("Listing %d configured servers", len(config.servers))
132
+ print_server_list(config.servers)
133
+
134
+
135
+ async def cmd_remove_server(args) -> None:
136
+ """Remove a server."""
137
+ config_manager = ConfigManager(args.config)
138
+ config = config_manager.load_config()
139
+
140
+ if args.name not in config.servers:
141
+ logger.warning("Attempt to remove non-existent server: %s", args.name)
142
+ print_error(f"Server '{args.name}' not found")
143
+ sys.exit(1)
144
+
145
+ # Show server details before removal
146
+ server = config.servers[args.name]
147
+ print_info(f"Server to remove: {args.name}")
148
+ print(f" Source: {server.source}")
149
+ print(f" Prefix: {server.prefix}")
150
+
151
+ if not args.force and not confirm_action("Are you sure you want to remove this server?"):
152
+ logger.debug("User cancelled removal of server '%s'", args.name)
153
+ print_info("Removal cancelled")
154
+ return
155
+
156
+ config.remove_server(args.name)
157
+
158
+ if config_manager.save_config(config):
159
+ logger.info("Successfully removed server '%s'", args.name)
160
+ print_success(f"Removed server '{args.name}'")
161
+ else:
162
+ print_error("Failed to save configuration")
163
+ sys.exit(1)
164
+
165
+
166
+ async def cmd_enable_server(args) -> None:
167
+ """Enable a server."""
168
+ config_manager = ConfigManager(args.config)
169
+ config = config_manager.load_config()
170
+
171
+ if args.name not in config.servers:
172
+ print_error(f"Server '{args.name}' not found")
173
+ sys.exit(1)
174
+
175
+ server = config.servers[args.name]
176
+ if server.enabled:
177
+ print_info(f"Server '{args.name}' is already enabled")
178
+ return
179
+
180
+ server.enabled = True
181
+
182
+ if config_manager.save_config(config):
183
+ print_success(f"Enabled server '{args.name}'")
184
+ print_info("The server will be mounted on next startup")
185
+ else:
186
+ print_error("Failed to save configuration")
187
+ sys.exit(1)
188
+
189
+
190
+ async def cmd_disable_server(args) -> None:
191
+ """Disable a server."""
192
+ config_manager = ConfigManager(args.config)
193
+ config = config_manager.load_config()
194
+
195
+ if args.name not in config.servers:
196
+ print_error(f"Server '{args.name}' not found")
197
+ sys.exit(1)
198
+
199
+ server = config.servers[args.name]
200
+ if not server.enabled:
201
+ print_info(f"Server '{args.name}' is already disabled")
202
+ return
203
+
204
+ server.enabled = False
205
+
206
+ if config_manager.save_config(config):
207
+ print_success(f"Disabled server '{args.name}'")
208
+ print_warning("The server will remain mounted until MAGG is restarted")
209
+ else:
210
+ print_error("Failed to save configuration")
211
+ sys.exit(1)
212
+
213
+
214
+ async def cmd_status(args) -> None:
215
+ """Show MAGG status."""
216
+ config_manager = ConfigManager(args.config)
217
+ config = config_manager.load_config()
218
+
219
+ enabled = [s for s in config.servers.values() if s.enabled]
220
+ disabled = [s for s in config.servers.values() if not s.enabled]
221
+
222
+ print_status_summary(
223
+ str(config_manager.config_path),
224
+ len(config.servers),
225
+ len(enabled),
226
+ len(disabled)
227
+ )
228
+
229
+
230
+ async def cmd_export(args) -> None:
231
+ """Export configuration."""
232
+ config_manager = ConfigManager(args.config)
233
+ config = config_manager.load_config()
234
+
235
+ export_data = {
236
+ 'servers': {
237
+ name: server.model_dump(
238
+ exclude_none=True, exclude_unset=True, exclude_defaults=True, by_alias=True
239
+ )
240
+ for name, server in config.servers.items()
241
+ }
242
+ }
243
+
244
+ if args.output:
245
+ try:
246
+ with open(args.output, 'w') as f:
247
+ json.dump(export_data, f, indent=2)
248
+ print_success(f"Exported configuration to {args.output}")
249
+ except IOError as e:
250
+ print_error(f"Failed to write to {args.output}: {e}")
251
+ sys.exit(1)
252
+ else:
253
+ print(json.dumps(export_data, indent=2))
254
+
255
+
256
+ def create_parser() -> argparse.ArgumentParser:
257
+ """Create the command line parser."""
258
+ parser = argparse.ArgumentParser(
259
+ prog='magg',
260
+ description='MAGG - MCP Aggregator: Manage and aggregate MCP servers',
261
+ epilog='Use "magg <command> --help" for more information about a command.'
262
+ )
263
+
264
+ parser.add_argument(
265
+ '--version', '-V',
266
+ action='version',
267
+ version=f'%(prog)s {__version__}',
268
+ )
269
+
270
+ parser.add_argument(
271
+ '--config',
272
+ type=str,
273
+ help='Path to config file (default: .magg/config.json in current directory)'
274
+ )
275
+
276
+ subparsers = parser.add_subparsers(dest='subcommand', help='Commands')
277
+
278
+ # Serve command
279
+ serve_parser = subparsers.add_parser(
280
+ 'serve',
281
+ help='Start MAGG server',
282
+ description='Start the MAGG server in either stdio mode (default) or HTTP mode'
283
+ )
284
+ cmd_serve_args(serve_parser)
285
+
286
+ # Add server command
287
+ add_parser = subparsers.add_parser('add-server', help='Add a new server')
288
+ add_parser.add_argument('name', help='Server name')
289
+ add_parser.add_argument('source', help='URL of the server package/repository')
290
+ add_parser.add_argument('--prefix', help='Tool prefix (defaults to server name)')
291
+ add_parser.add_argument('--command', help='Command to run the server')
292
+ add_parser.add_argument('--uri', help='URI for HTTP servers')
293
+ add_parser.add_argument('--env', nargs='*', help='Environment variables (KEY=VALUE)')
294
+ add_parser.add_argument('--working-dir', help='Working directory')
295
+ add_parser.add_argument('--notes', help='Setup notes')
296
+
297
+ # List servers
298
+ subparsers.add_parser('list-servers', help='List configured servers')
299
+
300
+ # Remove server
301
+ remove_parser = subparsers.add_parser('remove-server', help='Remove a server')
302
+ remove_parser.add_argument('name', help='Server name')
303
+ remove_parser.add_argument('--force', '-f', action='store_true', help='Remove without confirmation')
304
+
305
+ # Enable/disable server
306
+ enable_parser = subparsers.add_parser('enable-server', help='Enable a server')
307
+ enable_parser.add_argument('name', help='Server name')
308
+
309
+ disable_parser = subparsers.add_parser('disable-server', help='Disable a server')
310
+ disable_parser.add_argument('name', help='Server name')
311
+
312
+ # Status command
313
+ subparsers.add_parser('status', help='Show MAGG status')
314
+
315
+ # Export command
316
+ export_parser = subparsers.add_parser('export', help='Export configuration')
317
+ export_parser.add_argument('--output', '-o', help='Output file (default: stdout)')
318
+
319
+ return parser
320
+
321
+
322
+ async def run():
323
+ """Main entry point (async)."""
324
+ parser = create_parser()
325
+ args = parser.parse_args()
326
+
327
+ if not args.subcommand:
328
+ parser.print_help()
329
+ sys.exit(1)
330
+
331
+ # Map commands to functions
332
+ commands = {
333
+ 'serve': cmd_serve,
334
+ 'add-server': cmd_add_server,
335
+ 'list-servers': cmd_list_servers,
336
+ 'remove-server': cmd_remove_server,
337
+ 'enable-server': cmd_enable_server,
338
+ 'disable-server': cmd_disable_server,
339
+ 'status': cmd_status,
340
+ 'export': cmd_export,
341
+ }
342
+
343
+ cmd_func = commands.get(args.subcommand)
344
+ if cmd_func:
345
+ await cmd_func(args)
346
+ else:
347
+ parser.print_help()
348
+ sys.exit(1)
349
+
350
+
351
+ def main():
352
+ """Run the CLI."""
353
+ global logger
354
+
355
+ process.setup()
356
+
357
+ logger = logging.getLogger(__name__)
358
+
359
+ try:
360
+ asyncio.run(run())
361
+ except KeyboardInterrupt:
362
+ print_warning("\nOperation cancelled by user")
363
+ sys.exit(130) # Standard exit code for SIGINT
364
+ except Exception as e:
365
+ print_error(f"Unexpected error: {e}")
366
+ if os.getenv('MAGG_DEBUG').lower() in {'1', 'true', 'yes'}:
367
+ import traceback
368
+ traceback.print_exc()
369
+ sys.exit(1)
370
+
371
+
372
+ if __name__ == '__main__':
373
+ main()
@@ -0,0 +1 @@
1
+ """Tool discovery and search capabilities."""
@@ -0,0 +1,90 @@
1
+ """Simplified tool catalog for search functionality only."""
2
+
3
+ import json
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from .search import ToolSearchEngine, ToolSearchResult, ToolCatalog
9
+
10
+
11
+ class CatalogManager:
12
+ """Manages tool search catalog - search functionality only."""
13
+
14
+ def __init__(self, catalog_path: Path | None = None):
15
+ self.catalog_path = catalog_path or Path.cwd() / ".magg" / "search_cache.json"
16
+ self.catalog_path.parent.mkdir(parents=True, exist_ok=True)
17
+
18
+ self.search_catalog = ToolCatalog()
19
+ self.logger = logging.getLogger(__name__)
20
+
21
+ # Load existing search cache
22
+ self.load_search_cache()
23
+
24
+ def load_search_cache(self) -> None:
25
+ """Load search cache from disk."""
26
+ if not self.catalog_path.exists():
27
+ return
28
+
29
+ try:
30
+ with open(self.catalog_path, 'r') as f:
31
+ data = json.load(f)
32
+
33
+ # Load search catalog cache
34
+ if "search_catalog" in data:
35
+ self.search_catalog.import_catalog(data["search_catalog"])
36
+
37
+ except Exception as e:
38
+ self.logger.error(f"Error loading search cache: {e}")
39
+
40
+ def save_search_cache(self) -> None:
41
+ """Save search cache to disk."""
42
+ try:
43
+ data = {
44
+ "search_catalog": self.search_catalog.export_catalog()
45
+ }
46
+
47
+ with open(self.catalog_path, 'w') as f:
48
+ json.dump(data, f, indent=2)
49
+
50
+ except Exception as e:
51
+ self.logger.error(f"Error saving search cache: {e}")
52
+
53
+ async def search_only(self, query: str, limit_per_source: int = 5) -> dict[str, list[ToolSearchResult]]:
54
+ """Search for tools without auto-adding to cache."""
55
+ async with ToolSearchEngine() as search_engine:
56
+ results = await search_engine.search_all(query, limit_per_source)
57
+ return results
58
+
59
+ async def search_and_cache(self, query: str, limit_per_source: int = 5) -> dict[str, list[ToolSearchResult]]:
60
+ """Search for tools and update the cache."""
61
+ async with ToolSearchEngine() as search_engine:
62
+ results = await search_engine.search_all(query, limit_per_source)
63
+
64
+ # Add all results to cache
65
+ for source_results in results.values():
66
+ self.search_catalog.add_results(source_results)
67
+
68
+ # Save updated cache
69
+ self.save_search_cache()
70
+
71
+ return results
72
+
73
+ def search_local_cache(self, query: str) -> list[ToolSearchResult]:
74
+ """Search the local cache."""
75
+ return self.search_catalog.search_catalog(query)
76
+
77
+ def get_search_stats(self) -> dict[str, Any]:
78
+ """Get statistics about the search cache."""
79
+ total_cached = len(self.search_catalog.catalog)
80
+
81
+ # Count by source
82
+ source_counts = {}
83
+ for result in self.search_catalog.catalog.values():
84
+ source_counts[result.source] = source_counts.get(result.source, 0) + 1
85
+
86
+ return {
87
+ "total_cached": total_cached,
88
+ "source_breakdown": source_counts,
89
+ "cache_path": str(self.catalog_path)
90
+ }