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.
- ecosyste_ms_cli-1.3.2.dist-info/METADATA +133 -0
- ecosyste_ms_cli-1.3.2.dist-info/RECORD +84 -0
- ecosyste_ms_cli-1.3.2.dist-info/WHEEL +5 -0
- ecosyste_ms_cli-1.3.2.dist-info/entry_points.txt +2 -0
- ecosyste_ms_cli-1.3.2.dist-info/licenses/LICENSE +21 -0
- ecosyste_ms_cli-1.3.2.dist-info/top_level.txt +1 -0
- ecosystems_cli/__init__.py +3 -0
- ecosystems_cli/__main__.py +4 -0
- ecosystems_cli/apis/__init__.py +0 -0
- ecosystems_cli/apis/advisories.openapi.yaml +347 -0
- ecosystems_cli/apis/archives.openapi.yaml +193 -0
- ecosystems_cli/apis/commits.openapi.yaml +391 -0
- ecosystems_cli/apis/dependabot.openapi.yaml +887 -0
- ecosystems_cli/apis/diff.openapi.yaml +90 -0
- ecosystems_cli/apis/docker.openapi.yaml +534 -0
- ecosystems_cli/apis/issues.openapi.yaml +839 -0
- ecosystems_cli/apis/licenses.openapi.yaml +80 -0
- ecosystems_cli/apis/opencollective.openapi.yaml +247 -0
- ecosystems_cli/apis/packages.openapi.yaml +2522 -0
- ecosystems_cli/apis/parser.openapi.yaml +97 -0
- ecosystems_cli/apis/registries.yaml +155 -0
- ecosystems_cli/apis/repos.openapi.yaml +1521 -0
- ecosystems_cli/apis/resolve.openapi.yaml +130 -0
- ecosystems_cli/apis/sbom.openapi.yaml +79 -0
- ecosystems_cli/apis/sponsors.openapi.yaml +283 -0
- ecosystems_cli/apis/summary.openapi.yaml +239 -0
- ecosystems_cli/apis/timeline.openapi.yaml +91 -0
- ecosystems_cli/cli.py +213 -0
- ecosystems_cli/commands/__init__.py +1 -0
- ecosystems_cli/commands/advisories.py +109 -0
- ecosystems_cli/commands/archives.py +5 -0
- ecosystems_cli/commands/commits.py +5 -0
- ecosystems_cli/commands/decorators.py +101 -0
- ecosystems_cli/commands/dependabot.py +5 -0
- ecosystems_cli/commands/diff.py +144 -0
- ecosystems_cli/commands/docker.py +5 -0
- ecosystems_cli/commands/execution.py +99 -0
- ecosystems_cli/commands/generator.py +127 -0
- ecosystems_cli/commands/handlers/__init__.py +45 -0
- ecosystems_cli/commands/handlers/advisories.py +73 -0
- ecosystems_cli/commands/handlers/archives.py +40 -0
- ecosystems_cli/commands/handlers/base.py +38 -0
- ecosystems_cli/commands/handlers/commits.py +76 -0
- ecosystems_cli/commands/handlers/default.py +40 -0
- ecosystems_cli/commands/handlers/dependabot.py +205 -0
- ecosystems_cli/commands/handlers/diff.py +72 -0
- ecosystems_cli/commands/handlers/docker.py +142 -0
- ecosystems_cli/commands/handlers/factory.py +60 -0
- ecosystems_cli/commands/handlers/issues.py +87 -0
- ecosystems_cli/commands/handlers/licenses.py +52 -0
- ecosystems_cli/commands/handlers/opencollective.py +86 -0
- ecosystems_cli/commands/handlers/packages.py +103 -0
- ecosystems_cli/commands/handlers/parser.py +57 -0
- ecosystems_cli/commands/handlers/repos.py +97 -0
- ecosystems_cli/commands/handlers/resolve.py +68 -0
- ecosystems_cli/commands/handlers/sbom.py +52 -0
- ecosystems_cli/commands/handlers/sponsors.py +52 -0
- ecosystems_cli/commands/handlers/summary.py +81 -0
- ecosystems_cli/commands/handlers/timeline.py +45 -0
- ecosystems_cli/commands/issues.py +5 -0
- ecosystems_cli/commands/licenses.py +135 -0
- ecosystems_cli/commands/mcp.py +54 -0
- ecosystems_cli/commands/opencollective.py +5 -0
- ecosystems_cli/commands/packages.py +151 -0
- ecosystems_cli/commands/parser.py +135 -0
- ecosystems_cli/commands/repos.py +5 -0
- ecosystems_cli/commands/resolve.py +160 -0
- ecosystems_cli/commands/sbom.py +135 -0
- ecosystems_cli/commands/sponsors.py +5 -0
- ecosystems_cli/commands/summary.py +5 -0
- ecosystems_cli/commands/timeline.py +5 -0
- ecosystems_cli/constants.py +92 -0
- ecosystems_cli/exceptions.py +149 -0
- ecosystems_cli/helpers/click_params.py +49 -0
- ecosystems_cli/helpers/flatten_dict.py +15 -0
- ecosystems_cli/helpers/format_value.py +30 -0
- ecosystems_cli/helpers/get_domain.py +68 -0
- ecosystems_cli/helpers/load_api_spec.py +31 -0
- ecosystems_cli/helpers/print_error.py +15 -0
- ecosystems_cli/helpers/print_operations.py +73 -0
- ecosystems_cli/helpers/print_output.py +183 -0
- ecosystems_cli/helpers/purl_parser.py +121 -0
- ecosystems_cli/mcp_server.py +267 -0
- 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()
|