ecosyste-ms-cli 1.3.2__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 (84) hide show
  1. ecosyste_ms_cli-1.3.2.dist-info/METADATA +133 -0
  2. ecosyste_ms_cli-1.3.2.dist-info/RECORD +84 -0
  3. ecosyste_ms_cli-1.3.2.dist-info/WHEEL +5 -0
  4. ecosyste_ms_cli-1.3.2.dist-info/entry_points.txt +2 -0
  5. ecosyste_ms_cli-1.3.2.dist-info/licenses/LICENSE +21 -0
  6. ecosyste_ms_cli-1.3.2.dist-info/top_level.txt +1 -0
  7. ecosystems_cli/__init__.py +3 -0
  8. ecosystems_cli/__main__.py +4 -0
  9. ecosystems_cli/apis/__init__.py +0 -0
  10. ecosystems_cli/apis/advisories.openapi.yaml +347 -0
  11. ecosystems_cli/apis/archives.openapi.yaml +193 -0
  12. ecosystems_cli/apis/commits.openapi.yaml +391 -0
  13. ecosystems_cli/apis/dependabot.openapi.yaml +887 -0
  14. ecosystems_cli/apis/diff.openapi.yaml +90 -0
  15. ecosystems_cli/apis/docker.openapi.yaml +534 -0
  16. ecosystems_cli/apis/issues.openapi.yaml +839 -0
  17. ecosystems_cli/apis/licenses.openapi.yaml +80 -0
  18. ecosystems_cli/apis/opencollective.openapi.yaml +247 -0
  19. ecosystems_cli/apis/packages.openapi.yaml +2522 -0
  20. ecosystems_cli/apis/parser.openapi.yaml +97 -0
  21. ecosystems_cli/apis/registries.yaml +155 -0
  22. ecosystems_cli/apis/repos.openapi.yaml +1521 -0
  23. ecosystems_cli/apis/resolve.openapi.yaml +130 -0
  24. ecosystems_cli/apis/sbom.openapi.yaml +79 -0
  25. ecosystems_cli/apis/sponsors.openapi.yaml +283 -0
  26. ecosystems_cli/apis/summary.openapi.yaml +239 -0
  27. ecosystems_cli/apis/timeline.openapi.yaml +91 -0
  28. ecosystems_cli/cli.py +213 -0
  29. ecosystems_cli/commands/__init__.py +1 -0
  30. ecosystems_cli/commands/advisories.py +109 -0
  31. ecosystems_cli/commands/archives.py +5 -0
  32. ecosystems_cli/commands/commits.py +5 -0
  33. ecosystems_cli/commands/decorators.py +101 -0
  34. ecosystems_cli/commands/dependabot.py +5 -0
  35. ecosystems_cli/commands/diff.py +144 -0
  36. ecosystems_cli/commands/docker.py +5 -0
  37. ecosystems_cli/commands/execution.py +99 -0
  38. ecosystems_cli/commands/generator.py +127 -0
  39. ecosystems_cli/commands/handlers/__init__.py +45 -0
  40. ecosystems_cli/commands/handlers/advisories.py +73 -0
  41. ecosystems_cli/commands/handlers/archives.py +40 -0
  42. ecosystems_cli/commands/handlers/base.py +38 -0
  43. ecosystems_cli/commands/handlers/commits.py +76 -0
  44. ecosystems_cli/commands/handlers/default.py +40 -0
  45. ecosystems_cli/commands/handlers/dependabot.py +205 -0
  46. ecosystems_cli/commands/handlers/diff.py +72 -0
  47. ecosystems_cli/commands/handlers/docker.py +142 -0
  48. ecosystems_cli/commands/handlers/factory.py +60 -0
  49. ecosystems_cli/commands/handlers/issues.py +87 -0
  50. ecosystems_cli/commands/handlers/licenses.py +52 -0
  51. ecosystems_cli/commands/handlers/opencollective.py +86 -0
  52. ecosystems_cli/commands/handlers/packages.py +103 -0
  53. ecosystems_cli/commands/handlers/parser.py +57 -0
  54. ecosystems_cli/commands/handlers/repos.py +97 -0
  55. ecosystems_cli/commands/handlers/resolve.py +68 -0
  56. ecosystems_cli/commands/handlers/sbom.py +52 -0
  57. ecosystems_cli/commands/handlers/sponsors.py +52 -0
  58. ecosystems_cli/commands/handlers/summary.py +81 -0
  59. ecosystems_cli/commands/handlers/timeline.py +45 -0
  60. ecosystems_cli/commands/issues.py +5 -0
  61. ecosystems_cli/commands/licenses.py +135 -0
  62. ecosystems_cli/commands/mcp.py +54 -0
  63. ecosystems_cli/commands/opencollective.py +5 -0
  64. ecosystems_cli/commands/packages.py +151 -0
  65. ecosystems_cli/commands/parser.py +135 -0
  66. ecosystems_cli/commands/repos.py +5 -0
  67. ecosystems_cli/commands/resolve.py +160 -0
  68. ecosystems_cli/commands/sbom.py +135 -0
  69. ecosystems_cli/commands/sponsors.py +5 -0
  70. ecosystems_cli/commands/summary.py +5 -0
  71. ecosystems_cli/commands/timeline.py +5 -0
  72. ecosystems_cli/constants.py +92 -0
  73. ecosystems_cli/exceptions.py +149 -0
  74. ecosystems_cli/helpers/click_params.py +49 -0
  75. ecosystems_cli/helpers/flatten_dict.py +15 -0
  76. ecosystems_cli/helpers/format_value.py +30 -0
  77. ecosystems_cli/helpers/get_domain.py +68 -0
  78. ecosystems_cli/helpers/load_api_spec.py +31 -0
  79. ecosystems_cli/helpers/print_error.py +15 -0
  80. ecosystems_cli/helpers/print_operations.py +73 -0
  81. ecosystems_cli/helpers/print_output.py +183 -0
  82. ecosystems_cli/helpers/purl_parser.py +121 -0
  83. ecosystems_cli/mcp_server.py +267 -0
  84. ecosystems_cli/openapi_client.py +461 -0
@@ -0,0 +1,267 @@
1
+ """MCP (Model Context Protocol) server for Ecosystems CLI."""
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ import signal
7
+ from typing import Any, Dict, List
8
+
9
+ from mcp.server import Server
10
+ from mcp.server.models import InitializationOptions
11
+ from mcp.server.stdio import stdio_server
12
+ from mcp.types import ServerCapabilities, TextContent, Tool
13
+
14
+ from ecosystems_cli.constants import DEFAULT_TIMEOUT
15
+ from ecosystems_cli.exceptions import EcosystemsCLIError
16
+ from ecosystems_cli.helpers.get_domain import build_base_url, get_domain_with_precedence
17
+ from ecosystems_cli.helpers.load_api_spec import load_api_spec
18
+ from ecosystems_cli.helpers.print_output import DateTimeEncoder
19
+ from ecosystems_cli.openapi_client import _factory as api_factory
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class EcosystemsMCPServer:
25
+ """MCP server providing Ecosystems CLI functionality as tools."""
26
+
27
+ def __init__(self):
28
+ self.server = Server("ecosystems-cli")
29
+ self.apis = [
30
+ "advisories",
31
+ "archives",
32
+ "dependabot",
33
+ "diff",
34
+ "repos",
35
+ "packages",
36
+ "issues",
37
+ "licenses",
38
+ "sponsors",
39
+ "timeline",
40
+ "docker",
41
+ "opencollective",
42
+ "parser",
43
+ "resolve",
44
+ "sbom",
45
+ ]
46
+ self._register_handlers()
47
+
48
+ def _register_handlers(self):
49
+ """Register MCP protocol handlers."""
50
+
51
+ @self.server.list_tools()
52
+ async def list_tools() -> List[Tool]:
53
+ """List all available tools from the Ecosystems APIs."""
54
+ tools = []
55
+
56
+ for api in self.apis:
57
+ try:
58
+ spec = load_api_spec(api)
59
+ if not spec or "paths" not in spec:
60
+ continue
61
+
62
+ # Create a tool for each operation
63
+ for path, methods in spec["paths"].items():
64
+ for method, operation in methods.items():
65
+ if method in ["get", "post", "put", "delete", "patch"]:
66
+ operation_id = operation.get("operationId")
67
+ if not operation_id:
68
+ continue
69
+
70
+ # Build tool description
71
+ description = operation.get("summary", operation.get("description", ""))
72
+ if not description:
73
+ description = f"{method.upper()} {path} on {api} API"
74
+
75
+ # Build input schema
76
+ input_schema = self._build_input_schema(operation)
77
+
78
+ tools.append(
79
+ Tool(name=f"{api}_{operation_id}", description=description, inputSchema=input_schema)
80
+ )
81
+
82
+ # Add a generic call tool for each API
83
+ tools.append(
84
+ Tool(
85
+ name=f"{api}_call",
86
+ description=f"Call any operation on the {api} API directly",
87
+ inputSchema={
88
+ "type": "object",
89
+ "properties": {
90
+ "operation": {"type": "string", "description": "The operation ID to call"},
91
+ "path_params": {"type": "object", "description": "Path parameters as a JSON object"},
92
+ "query_params": {"type": "object", "description": "Query parameters as a JSON object"},
93
+ "body": {"type": "object", "description": "Request body as a JSON object"},
94
+ },
95
+ "required": ["operation"],
96
+ },
97
+ )
98
+ )
99
+
100
+ except Exception as e:
101
+ logger.error(f"Error loading spec for {api}: {e}")
102
+ continue
103
+
104
+ return tools
105
+
106
+ @self.server.call_tool()
107
+ async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
108
+ """Execute a tool call."""
109
+ try:
110
+ # Parse the tool name to get API and operation
111
+ if name.endswith("_call"):
112
+ # Generic call tool
113
+ api = name[:-5] # Remove '_call' suffix
114
+ if api not in self.apis:
115
+ return [TextContent(type="text", text=f"Unknown API: {api}")]
116
+ operation = arguments.get("operation")
117
+ path_params = arguments.get("path_params", {})
118
+ query_params = arguments.get("query_params", {})
119
+ body = arguments.get("body", {})
120
+ else:
121
+ # Specific operation tool
122
+ parts = name.split("_", 1)
123
+ if len(parts) != 2:
124
+ return [TextContent(type="text", text=f"Invalid tool name: {name}")]
125
+
126
+ api, operation = parts
127
+ if api not in self.apis:
128
+ return [TextContent(type="text", text=f"Unknown API: {api}")]
129
+
130
+ # Extract parameters from arguments
131
+ path_params = {}
132
+ query_params = {}
133
+ body = {}
134
+
135
+ # Load spec to determine parameter types
136
+ spec = load_api_spec(api)
137
+ if spec and "paths" in spec:
138
+ for path, methods in spec["paths"].items():
139
+ for method, op_spec in methods.items():
140
+ if op_spec.get("operationId") == operation:
141
+ # Extract parameters based on spec
142
+ for param in op_spec.get("parameters", []):
143
+ param_name = param.get("name")
144
+ param_in = param.get("in")
145
+
146
+ if param_name in arguments:
147
+ if param_in == "path":
148
+ path_params[param_name] = arguments[param_name]
149
+ elif param_in == "query":
150
+ query_params[param_name] = arguments[param_name]
151
+
152
+ # Check for request body
153
+ if "requestBody" in op_spec and "body" in arguments:
154
+ body = arguments["body"]
155
+
156
+ break
157
+
158
+ # Call the API
159
+ result = await self._call_api(api, operation, path_params, query_params, body)
160
+
161
+ # Format the result as JSON string
162
+ result_text = json.dumps(result, cls=DateTimeEncoder) if result else "No data returned"
163
+
164
+ return [TextContent(type="text", text=result_text)]
165
+
166
+ except Exception as e:
167
+ logger.error(f"Error calling tool {name}: {e}")
168
+ return [TextContent(type="text", text=f"Error: {str(e)}")]
169
+
170
+ def _build_input_schema(self, operation: Dict[str, Any]) -> Dict[str, Any]:
171
+ """Build JSON schema for tool input from OpenAPI operation."""
172
+ schema = {"type": "object", "properties": {}, "required": []}
173
+
174
+ # Add parameters
175
+ for param in operation.get("parameters", []):
176
+ param_name = param.get("name")
177
+ param_schema = param.get("schema", {})
178
+ param_required = param.get("required", False)
179
+
180
+ schema["properties"][param_name] = {
181
+ "type": param_schema.get("type", "string"),
182
+ "description": param.get("description", ""),
183
+ }
184
+
185
+ if param_required:
186
+ schema["required"].append(param_name)
187
+
188
+ # Add request body if present
189
+ if "requestBody" in operation:
190
+ request_body = operation["requestBody"]
191
+ if request_body.get("required", False):
192
+ schema["required"].append("body")
193
+
194
+ # Try to get schema from content
195
+ content = request_body.get("content", {})
196
+ if "application/json" in content:
197
+ schema["properties"]["body"] = {
198
+ "type": "object",
199
+ "description": request_body.get("description", "Request body"),
200
+ }
201
+
202
+ return schema
203
+
204
+ async def _call_api(
205
+ self, api: str, operation: str, path_params: Dict[str, Any], query_params: Dict[str, Any], body: Dict[str, Any]
206
+ ) -> Any:
207
+ """Call an API operation and return the result."""
208
+ # Get domain and build URL
209
+ domain = get_domain_with_precedence(api, None)
210
+ base_url = build_base_url(domain, api)
211
+
212
+ # Call the operation using API factory
213
+ try:
214
+ result = api_factory.call(
215
+ api_name=api,
216
+ operation_id=operation,
217
+ path_params=path_params if path_params else None,
218
+ query_params=query_params if query_params else None,
219
+ body=body if body else None,
220
+ timeout=DEFAULT_TIMEOUT,
221
+ base_url=base_url,
222
+ )
223
+ return result
224
+ except EcosystemsCLIError as e:
225
+ raise Exception(f"API Error: {str(e)}")
226
+
227
+ async def run(self):
228
+ """Run the MCP server."""
229
+ async with stdio_server() as (read_stream, write_stream):
230
+ await self.server.run(
231
+ read_stream,
232
+ write_stream,
233
+ InitializationOptions(
234
+ server_name="ecosystems-cli", server_version="1.0.0", capabilities=ServerCapabilities(tools={})
235
+ ),
236
+ )
237
+
238
+
239
+ def run_mcp_server():
240
+ """Entry point for running the MCP server."""
241
+ server = EcosystemsMCPServer()
242
+
243
+ # Set up signal handlers for graceful shutdown
244
+ loop = asyncio.new_event_loop()
245
+ asyncio.set_event_loop(loop)
246
+
247
+ shutdown_event = asyncio.Event()
248
+
249
+ def signal_handler(sig, frame):
250
+ """Handle shutdown signals gracefully."""
251
+ logger.info(f"Received signal {sig}, initiating graceful shutdown...")
252
+ loop.call_soon_threadsafe(shutdown_event.set)
253
+
254
+ # Register signal handlers
255
+ signal.signal(signal.SIGINT, signal_handler)
256
+ signal.signal(signal.SIGTERM, signal_handler)
257
+
258
+ try:
259
+ loop.run_until_complete(server.run())
260
+ except KeyboardInterrupt:
261
+ logger.info("Keyboard interrupt received, shutting down gracefully...")
262
+ finally:
263
+ loop.close()
264
+
265
+
266
+ if __name__ == "__main__":
267
+ run_mcp_server()