cloudsmith-cli 1.11.0__py2.py3-none-any.whl → 1.11.1__py2.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.
- cloudsmith_cli/cli/commands/__init__.py +0 -1
- cloudsmith_cli/cli/config.py +0 -31
- cloudsmith_cli/cli/decorators.py +3 -47
- cloudsmith_cli/data/VERSION +1 -1
- {cloudsmith_cli-1.11.0.dist-info → cloudsmith_cli-1.11.1.dist-info}/METADATA +2 -4
- {cloudsmith_cli-1.11.0.dist-info → cloudsmith_cli-1.11.1.dist-info}/RECORD +10 -15
- cloudsmith_cli/cli/commands/mcp.py +0 -424
- cloudsmith_cli/cli/tests/commands/test_mcp.py +0 -357
- cloudsmith_cli/core/mcp/__init__.py +0 -0
- cloudsmith_cli/core/mcp/data.py +0 -17
- cloudsmith_cli/core/mcp/server.py +0 -779
- {cloudsmith_cli-1.11.0.dist-info → cloudsmith_cli-1.11.1.dist-info}/WHEEL +0 -0
- {cloudsmith_cli-1.11.0.dist-info → cloudsmith_cli-1.11.1.dist-info}/entry_points.txt +0 -0
- {cloudsmith_cli-1.11.0.dist-info → cloudsmith_cli-1.11.1.dist-info}/licenses/LICENSE +0 -0
- {cloudsmith_cli-1.11.0.dist-info → cloudsmith_cli-1.11.1.dist-info}/top_level.txt +0 -0
|
@@ -1,357 +0,0 @@
|
|
|
1
|
-
from unittest.mock import patch
|
|
2
|
-
|
|
3
|
-
import cloudsmith_api
|
|
4
|
-
|
|
5
|
-
from ....cli.commands.mcp import list_groups, list_tools
|
|
6
|
-
from ....core.mcp.data import OpenAPITool
|
|
7
|
-
from ....core.mcp.server import DynamicMCPServer
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class TestMCPListToolsCommand:
|
|
11
|
-
def test_list_tools_command_basic(self, runner):
|
|
12
|
-
"""Test that list_tools command returns available tools."""
|
|
13
|
-
# Mock tools data
|
|
14
|
-
mock_tools = {
|
|
15
|
-
"repos_list": OpenAPITool(
|
|
16
|
-
name="repos_list",
|
|
17
|
-
description="List repositories",
|
|
18
|
-
method="GET",
|
|
19
|
-
path="/repos/",
|
|
20
|
-
parameters={"type": "object", "properties": {}, "required": []},
|
|
21
|
-
base_url="https://api.cloudsmith.io",
|
|
22
|
-
query_filter=None,
|
|
23
|
-
is_destructive=False,
|
|
24
|
-
is_read_only=True,
|
|
25
|
-
),
|
|
26
|
-
"packages_list": OpenAPITool(
|
|
27
|
-
name="packages_list",
|
|
28
|
-
description="List packages",
|
|
29
|
-
method="GET",
|
|
30
|
-
path="/packages/",
|
|
31
|
-
parameters={"type": "object", "properties": {}, "required": []},
|
|
32
|
-
base_url="https://api.cloudsmith.io",
|
|
33
|
-
query_filter=None,
|
|
34
|
-
is_destructive=False,
|
|
35
|
-
is_read_only=True,
|
|
36
|
-
),
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
with patch(
|
|
40
|
-
"cloudsmith_cli.core.mcp.server.DynamicMCPServer.list_tools"
|
|
41
|
-
) as list_tools_mock:
|
|
42
|
-
list_tools_mock.return_value = mock_tools
|
|
43
|
-
result = runner.invoke(list_tools, catch_exceptions=False)
|
|
44
|
-
|
|
45
|
-
assert result.exit_code == 0
|
|
46
|
-
assert "repos_list" in result.output
|
|
47
|
-
assert "packages_list" in result.output
|
|
48
|
-
assert "List repositories" in result.output
|
|
49
|
-
assert "List packages" in result.output
|
|
50
|
-
list_tools_mock.assert_called_once()
|
|
51
|
-
|
|
52
|
-
def test_list_tools_command_with_filtering(self, runner):
|
|
53
|
-
"""Test that list_tools command respects filtering configuration."""
|
|
54
|
-
# Mock tools data - simulating filtered results
|
|
55
|
-
mock_tools = {
|
|
56
|
-
"repos_list": OpenAPITool(
|
|
57
|
-
name="repos_list",
|
|
58
|
-
description="List repositories",
|
|
59
|
-
method="GET",
|
|
60
|
-
path="/repos/",
|
|
61
|
-
parameters={"type": "object", "properties": {}, "required": []},
|
|
62
|
-
base_url="https://api.cloudsmith.io",
|
|
63
|
-
query_filter=None,
|
|
64
|
-
is_destructive=False,
|
|
65
|
-
is_read_only=True,
|
|
66
|
-
),
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
with patch(
|
|
70
|
-
"cloudsmith_cli.core.mcp.server.DynamicMCPServer.list_tools"
|
|
71
|
-
) as list_tools_mock:
|
|
72
|
-
list_tools_mock.return_value = mock_tools
|
|
73
|
-
result = runner.invoke(list_tools, catch_exceptions=False)
|
|
74
|
-
|
|
75
|
-
assert result.exit_code == 0
|
|
76
|
-
assert "repos_list" in result.output
|
|
77
|
-
# Verify that filtered tools are not in the output
|
|
78
|
-
assert "packages_list" not in result.output
|
|
79
|
-
list_tools_mock.assert_called_once()
|
|
80
|
-
|
|
81
|
-
def test_list_tools_command_json_output(self, runner):
|
|
82
|
-
"""Test that list_tools command can output JSON format."""
|
|
83
|
-
mock_tools = {
|
|
84
|
-
"repos_list": OpenAPITool(
|
|
85
|
-
name="repos_list",
|
|
86
|
-
description="List repositories",
|
|
87
|
-
method="GET",
|
|
88
|
-
path="/repos/",
|
|
89
|
-
parameters={"type": "object", "properties": {}, "required": []},
|
|
90
|
-
base_url="https://api.cloudsmith.io",
|
|
91
|
-
query_filter=None,
|
|
92
|
-
is_destructive=False,
|
|
93
|
-
is_read_only=True,
|
|
94
|
-
),
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
with patch(
|
|
98
|
-
"cloudsmith_cli.core.mcp.server.DynamicMCPServer.list_tools"
|
|
99
|
-
) as list_tools_mock:
|
|
100
|
-
list_tools_mock.return_value = mock_tools
|
|
101
|
-
result = runner.invoke(
|
|
102
|
-
list_tools, ["--output-format", "json"], catch_exceptions=False
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
assert result.exit_code == 0
|
|
106
|
-
# JSON output should contain structured data
|
|
107
|
-
assert '"name":' in result.output or '"name": "repos_list"' in result.output
|
|
108
|
-
list_tools_mock.assert_called_once()
|
|
109
|
-
|
|
110
|
-
def test_list_tools_command_empty(self, runner):
|
|
111
|
-
"""Test that list_tools command handles empty tool list."""
|
|
112
|
-
mock_tools = {}
|
|
113
|
-
|
|
114
|
-
with patch(
|
|
115
|
-
"cloudsmith_cli.core.mcp.server.DynamicMCPServer.list_tools"
|
|
116
|
-
) as list_tools_mock:
|
|
117
|
-
list_tools_mock.return_value = mock_tools
|
|
118
|
-
result = runner.invoke(list_tools, catch_exceptions=False)
|
|
119
|
-
|
|
120
|
-
assert result.exit_code == 0
|
|
121
|
-
assert "0 tools visible" in result.output
|
|
122
|
-
list_tools_mock.assert_called_once()
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
class TestMCPListGroupsCommand:
|
|
126
|
-
def test_list_groups_command_basic(self, runner):
|
|
127
|
-
"""Test that list_groups command returns available tool groups."""
|
|
128
|
-
# Mock groups data
|
|
129
|
-
mock_groups = {
|
|
130
|
-
"repos": ["repos_list", "repos_create", "repos_delete"],
|
|
131
|
-
"packages": ["packages_list", "packages_read", "packages_delete"],
|
|
132
|
-
"orgs": ["orgs_list", "orgs_read"],
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
with patch(
|
|
136
|
-
"cloudsmith_cli.core.mcp.server.DynamicMCPServer.list_groups"
|
|
137
|
-
) as list_groups_mock:
|
|
138
|
-
list_groups_mock.return_value = mock_groups
|
|
139
|
-
result = runner.invoke(list_groups, catch_exceptions=False)
|
|
140
|
-
|
|
141
|
-
assert result.exit_code == 0
|
|
142
|
-
assert "repos" in result.output
|
|
143
|
-
assert "packages" in result.output
|
|
144
|
-
assert "orgs" in result.output
|
|
145
|
-
# Check tool counts are displayed
|
|
146
|
-
assert "3" in result.output # repos has 3 tools
|
|
147
|
-
assert "2" in result.output # orgs has 2 tools
|
|
148
|
-
list_groups_mock.assert_called_once()
|
|
149
|
-
|
|
150
|
-
def test_list_groups_command_with_filtering(self, runner):
|
|
151
|
-
"""Test that list_groups command respects filtering configuration."""
|
|
152
|
-
# Mock groups data - simulating filtered results with only repos group
|
|
153
|
-
mock_groups = {
|
|
154
|
-
"repos": ["repos_list", "repos_create"],
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
with patch(
|
|
158
|
-
"cloudsmith_cli.core.mcp.server.DynamicMCPServer.list_groups"
|
|
159
|
-
) as list_groups_mock:
|
|
160
|
-
list_groups_mock.return_value = mock_groups
|
|
161
|
-
result = runner.invoke(list_groups, catch_exceptions=False)
|
|
162
|
-
|
|
163
|
-
assert result.exit_code == 0
|
|
164
|
-
assert "repos" in result.output
|
|
165
|
-
# Verify that filtered groups are not in the output
|
|
166
|
-
assert "packages" not in result.output
|
|
167
|
-
assert "orgs" not in result.output
|
|
168
|
-
list_groups_mock.assert_called_once()
|
|
169
|
-
|
|
170
|
-
def test_list_groups_command_json_output(self, runner):
|
|
171
|
-
"""Test that list_groups command can output JSON format."""
|
|
172
|
-
mock_groups = {
|
|
173
|
-
"repos": ["repos_list", "repos_create"],
|
|
174
|
-
"packages": ["packages_list"],
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
with patch(
|
|
178
|
-
"cloudsmith_cli.core.mcp.server.DynamicMCPServer.list_groups"
|
|
179
|
-
) as list_groups_mock:
|
|
180
|
-
list_groups_mock.return_value = mock_groups
|
|
181
|
-
result = runner.invoke(
|
|
182
|
-
list_groups, ["--output-format", "json"], catch_exceptions=False
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
assert result.exit_code == 0
|
|
186
|
-
# JSON output should contain structured data
|
|
187
|
-
assert '"name":' in result.output or '"name": "repos"' in result.output
|
|
188
|
-
assert '"tools":' in result.output
|
|
189
|
-
list_groups_mock.assert_called_once()
|
|
190
|
-
|
|
191
|
-
def test_list_groups_command_empty(self, runner):
|
|
192
|
-
"""Test that list_groups command handles empty group list."""
|
|
193
|
-
mock_groups = {}
|
|
194
|
-
|
|
195
|
-
with patch(
|
|
196
|
-
"cloudsmith_cli.core.mcp.server.DynamicMCPServer.list_groups"
|
|
197
|
-
) as list_groups_mock:
|
|
198
|
-
list_groups_mock.return_value = mock_groups
|
|
199
|
-
result = runner.invoke(list_groups, catch_exceptions=False)
|
|
200
|
-
|
|
201
|
-
assert result.exit_code == 0
|
|
202
|
-
assert "0 groups visible" in result.output
|
|
203
|
-
list_groups_mock.assert_called_once()
|
|
204
|
-
|
|
205
|
-
def test_list_groups_command_with_many_tools(self, runner):
|
|
206
|
-
"""Test that list_groups command shows sample tools for groups with many tools."""
|
|
207
|
-
# Mock a group with more than 3 tools to test the "... (+N more)" feature
|
|
208
|
-
mock_groups = {
|
|
209
|
-
"repos": [
|
|
210
|
-
"repos_list",
|
|
211
|
-
"repos_create",
|
|
212
|
-
"repos_read",
|
|
213
|
-
"repos_update",
|
|
214
|
-
"repos_delete",
|
|
215
|
-
],
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
with patch(
|
|
219
|
-
"cloudsmith_cli.core.mcp.server.DynamicMCPServer.list_groups"
|
|
220
|
-
) as list_groups_mock:
|
|
221
|
-
list_groups_mock.return_value = mock_groups
|
|
222
|
-
result = runner.invoke(list_groups, catch_exceptions=False)
|
|
223
|
-
|
|
224
|
-
assert result.exit_code == 0
|
|
225
|
-
assert "repos" in result.output
|
|
226
|
-
assert "5" in result.output # Total count of tools
|
|
227
|
-
# Should show "... (+2 more)" since it only displays first 3
|
|
228
|
-
assert "+2 more" in result.output
|
|
229
|
-
list_groups_mock.assert_called_once()
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
class TestMCPServerDynamicToolGeneration:
|
|
233
|
-
def test_server_generates_tools_from_openapi_spec(self):
|
|
234
|
-
"""Test that MCP server dynamically creates tools from OpenAPI spec."""
|
|
235
|
-
# Create a minimal OpenAPI spec
|
|
236
|
-
mock_openapi_spec = {
|
|
237
|
-
"paths": {
|
|
238
|
-
"/repos/": {
|
|
239
|
-
"get": {
|
|
240
|
-
"operationId": "repos_list",
|
|
241
|
-
"summary": "List all repositories",
|
|
242
|
-
"parameters": [
|
|
243
|
-
{
|
|
244
|
-
"name": "page",
|
|
245
|
-
"in": "query",
|
|
246
|
-
"type": "integer",
|
|
247
|
-
"description": "Page number",
|
|
248
|
-
}
|
|
249
|
-
],
|
|
250
|
-
}
|
|
251
|
-
},
|
|
252
|
-
"/repos/{owner}/{identifier}/": {
|
|
253
|
-
"parameters": [
|
|
254
|
-
{
|
|
255
|
-
"name": "owner",
|
|
256
|
-
"in": "path",
|
|
257
|
-
"required": True,
|
|
258
|
-
"type": "string",
|
|
259
|
-
},
|
|
260
|
-
{
|
|
261
|
-
"name": "identifier",
|
|
262
|
-
"in": "path",
|
|
263
|
-
"required": True,
|
|
264
|
-
"type": "string",
|
|
265
|
-
},
|
|
266
|
-
],
|
|
267
|
-
"get": {
|
|
268
|
-
"operationId": "repos_read",
|
|
269
|
-
"summary": "Get a specific repository",
|
|
270
|
-
},
|
|
271
|
-
"delete": {
|
|
272
|
-
"operationId": "repos_delete",
|
|
273
|
-
"summary": "Delete a repository",
|
|
274
|
-
},
|
|
275
|
-
},
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
# Create API config
|
|
280
|
-
api_config = cloudsmith_api.Configuration()
|
|
281
|
-
api_config.host = "https://api.cloudsmith.io"
|
|
282
|
-
api_config.api_key = {"X-Api-Key": "test-key"}
|
|
283
|
-
|
|
284
|
-
# Create MCP server instance
|
|
285
|
-
server = DynamicMCPServer(api_config=api_config, force_all_tools=True)
|
|
286
|
-
|
|
287
|
-
# Mock the spec loading directly
|
|
288
|
-
server.spec = mock_openapi_spec
|
|
289
|
-
|
|
290
|
-
# Call the synchronous tool generation method
|
|
291
|
-
import asyncio
|
|
292
|
-
|
|
293
|
-
asyncio.run(
|
|
294
|
-
server._generate_tools_from_spec() # pylint: disable=protected-access
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
# Verify tools were created
|
|
298
|
-
assert len(server.tools) == 3
|
|
299
|
-
assert "repos_list" in server.tools
|
|
300
|
-
assert "repos_read" in server.tools
|
|
301
|
-
assert "repos_delete" in server.tools
|
|
302
|
-
|
|
303
|
-
# Verify tool details
|
|
304
|
-
repos_list_tool = server.tools["repos_list"]
|
|
305
|
-
assert repos_list_tool.name == "repos_list"
|
|
306
|
-
assert repos_list_tool.description == "List all repositories"
|
|
307
|
-
assert repos_list_tool.method == "GET"
|
|
308
|
-
assert repos_list_tool.path == "/repos/"
|
|
309
|
-
assert repos_list_tool.is_read_only is True
|
|
310
|
-
assert repos_list_tool.is_destructive is False
|
|
311
|
-
|
|
312
|
-
# Verify delete tool is marked as destructive
|
|
313
|
-
repos_delete_tool = server.tools["repos_delete"]
|
|
314
|
-
assert repos_delete_tool.is_destructive is True
|
|
315
|
-
assert repos_delete_tool.is_read_only is False
|
|
316
|
-
|
|
317
|
-
def test_server_respects_tool_filtering(self):
|
|
318
|
-
"""Test that MCP server filters tools based on configuration."""
|
|
319
|
-
# Create a simple OpenAPI spec
|
|
320
|
-
mock_openapi_spec = {
|
|
321
|
-
"paths": {
|
|
322
|
-
"/repos/": {
|
|
323
|
-
"get": {
|
|
324
|
-
"operationId": "repos_list",
|
|
325
|
-
"summary": "List repositories",
|
|
326
|
-
}
|
|
327
|
-
},
|
|
328
|
-
"/packages/": {
|
|
329
|
-
"get": {
|
|
330
|
-
"operationId": "packages_list",
|
|
331
|
-
"summary": "List packages",
|
|
332
|
-
}
|
|
333
|
-
},
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
api_config = cloudsmith_api.Configuration()
|
|
338
|
-
api_config.host = "https://api.cloudsmith.io"
|
|
339
|
-
api_config.api_key = {"X-Api-Key": "test-key"}
|
|
340
|
-
|
|
341
|
-
# Create server with filtering - only allow repos group
|
|
342
|
-
server = DynamicMCPServer(api_config=api_config, allowed_tool_groups=["repos"])
|
|
343
|
-
|
|
344
|
-
# Mock the spec loading directly
|
|
345
|
-
server.spec = mock_openapi_spec
|
|
346
|
-
|
|
347
|
-
# Call the tool generation method
|
|
348
|
-
import asyncio
|
|
349
|
-
|
|
350
|
-
asyncio.run(
|
|
351
|
-
server._generate_tools_from_spec() # pylint: disable=protected-access
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
# Verify only repos tools were created
|
|
355
|
-
assert len(server.tools) == 1
|
|
356
|
-
assert "repos_list" in server.tools
|
|
357
|
-
assert "packages_list" not in server.tools
|
|
File without changes
|
cloudsmith_cli/core/mcp/data.py
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
from typing import Any, Dict, Optional
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
@dataclass
|
|
6
|
-
class OpenAPITool:
|
|
7
|
-
"""Represents a tool generated from OpenAPI spec"""
|
|
8
|
-
|
|
9
|
-
name: str
|
|
10
|
-
description: str
|
|
11
|
-
method: str
|
|
12
|
-
path: str
|
|
13
|
-
parameters: Dict[str, Any]
|
|
14
|
-
base_url: str
|
|
15
|
-
query_filter: Optional[str]
|
|
16
|
-
is_destructive: bool = False
|
|
17
|
-
is_read_only: bool = False
|