cloudsmith-cli 1.11.1__py2.py3-none-any.whl → 1.12.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 +1 -0
- cloudsmith_cli/cli/commands/main.py +22 -12
- cloudsmith_cli/cli/commands/mcp.py +432 -0
- cloudsmith_cli/cli/commands/metrics/entitlements.py +1 -1
- cloudsmith_cli/cli/commands/metrics/packages.py +1 -1
- cloudsmith_cli/cli/commands/upstream.py +1 -0
- cloudsmith_cli/cli/config.py +31 -1
- cloudsmith_cli/cli/decorators.py +47 -3
- cloudsmith_cli/cli/tests/commands/test_main.py +28 -0
- cloudsmith_cli/cli/tests/commands/test_mcp.py +357 -0
- cloudsmith_cli/cli/tests/commands/test_repos.py +3 -3
- cloudsmith_cli/core/mcp/__init__.py +0 -0
- cloudsmith_cli/core/mcp/data.py +17 -0
- cloudsmith_cli/core/mcp/server.py +792 -0
- cloudsmith_cli/core/ratelimits.py +1 -1
- cloudsmith_cli/core/tests/test_init.py +1 -1
- cloudsmith_cli/data/VERSION +1 -1
- {cloudsmith_cli-1.11.1.dist-info → cloudsmith_cli-1.12.1.dist-info}/METADATA +7 -5
- {cloudsmith_cli-1.11.1.dist-info → cloudsmith_cli-1.12.1.dist-info}/RECORD +23 -18
- {cloudsmith_cli-1.11.1.dist-info → cloudsmith_cli-1.12.1.dist-info}/WHEEL +1 -1
- {cloudsmith_cli-1.11.1.dist-info → cloudsmith_cli-1.12.1.dist-info}/entry_points.txt +0 -0
- {cloudsmith_cli-1.11.1.dist-info → cloudsmith_cli-1.12.1.dist-info}/licenses/LICENSE +0 -0
- {cloudsmith_cli-1.11.1.dist-info → cloudsmith_cli-1.12.1.dist-info}/top_level.txt +0 -0
|
@@ -5,22 +5,31 @@ import click
|
|
|
5
5
|
from ...core.api.version import get_version as get_api_version
|
|
6
6
|
from ...core.utils import get_github_website, get_help_website
|
|
7
7
|
from ...core.version import get_version as get_cli_version
|
|
8
|
-
from .. import command, decorators
|
|
8
|
+
from .. import command, decorators, utils
|
|
9
9
|
|
|
10
10
|
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def print_version():
|
|
13
|
+
def print_version(opts):
|
|
14
14
|
"""Print the environment versions."""
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
)
|
|
15
|
+
cli_version = get_cli_version()
|
|
16
|
+
api_version = get_api_version()
|
|
17
|
+
|
|
18
|
+
data = {
|
|
19
|
+
"cli_version": cli_version,
|
|
20
|
+
"api_version": api_version,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if not utils.maybe_print_as_json(opts, data):
|
|
24
|
+
click.echo("Versions:")
|
|
25
|
+
click.secho(
|
|
26
|
+
"CLI Package Version: %(version)s"
|
|
27
|
+
% {"version": click.style(cli_version, bold=True)}
|
|
28
|
+
)
|
|
29
|
+
click.secho(
|
|
30
|
+
"API Package Version: %(version)s"
|
|
31
|
+
% {"version": click.style(api_version, bold=True)}
|
|
32
|
+
)
|
|
24
33
|
|
|
25
34
|
|
|
26
35
|
@click.group(
|
|
@@ -52,12 +61,13 @@ For issues/contributing: %(github_website)s
|
|
|
52
61
|
is_eager=True,
|
|
53
62
|
)
|
|
54
63
|
@decorators.common_cli_config_options
|
|
64
|
+
@decorators.common_cli_output_options
|
|
55
65
|
@click.pass_context
|
|
56
66
|
def main(ctx, opts, version):
|
|
57
67
|
"""Handle entrypoint to CLI."""
|
|
58
68
|
# pylint: disable=unused-argument
|
|
59
69
|
|
|
60
70
|
if version:
|
|
61
|
-
print_version()
|
|
71
|
+
print_version(opts)
|
|
62
72
|
elif ctx.invoked_subcommand is None:
|
|
63
73
|
click.echo(ctx.get_help())
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""Main command/entrypoint."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, List
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
import json5
|
|
12
|
+
|
|
13
|
+
from ...core.mcp import server
|
|
14
|
+
from ...core.mcp.data import OpenAPITool
|
|
15
|
+
from .. import command, decorators, utils
|
|
16
|
+
from .main import main
|
|
17
|
+
|
|
18
|
+
SUPPORTED_MCP_CLIENTS = {
|
|
19
|
+
"claude": "Claude Desktop",
|
|
20
|
+
"cursor": "Cursor IDE",
|
|
21
|
+
"vscode": "VS Code",
|
|
22
|
+
"gemini-cli": "Gemini CLI",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@main.group(cls=command.AliasGroup, name="mcp")
|
|
27
|
+
@decorators.common_cli_config_options
|
|
28
|
+
@decorators.common_cli_output_options
|
|
29
|
+
@decorators.common_api_auth_options
|
|
30
|
+
@decorators.initialise_api
|
|
31
|
+
@click.pass_context
|
|
32
|
+
def mcp_(ctx, opts): # pylint: disable=unused-argument
|
|
33
|
+
"""
|
|
34
|
+
Start the Cloudsmith MCP Server
|
|
35
|
+
|
|
36
|
+
See the help for subcommands for more information on each.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@mcp_.command(name="start")
|
|
41
|
+
@decorators.initialise_api
|
|
42
|
+
@decorators.initialise_mcp
|
|
43
|
+
@click.pass_context
|
|
44
|
+
def start(ctx, opts, mcp_server: server.DynamicMCPServer):
|
|
45
|
+
"""
|
|
46
|
+
Start the MCP Server
|
|
47
|
+
"""
|
|
48
|
+
mcp_server.run()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@mcp_.command(name="list_tools")
|
|
52
|
+
@decorators.common_cli_config_options
|
|
53
|
+
@decorators.common_cli_output_options
|
|
54
|
+
@decorators.common_api_auth_options
|
|
55
|
+
@decorators.initialise_api
|
|
56
|
+
@decorators.initialise_mcp
|
|
57
|
+
@click.pass_context
|
|
58
|
+
def list_tools(ctx, opts, mcp_server: server.DynamicMCPServer):
|
|
59
|
+
"""
|
|
60
|
+
List available tools that will be exposed to the MCP Client
|
|
61
|
+
"""
|
|
62
|
+
use_stderr = utils.should_use_stderr(opts)
|
|
63
|
+
|
|
64
|
+
if not use_stderr:
|
|
65
|
+
click.echo("Getting list of tools ... ", nl=False, err=use_stderr)
|
|
66
|
+
|
|
67
|
+
with utils.maybe_spinner(opts):
|
|
68
|
+
tools = mcp_server.list_tools()
|
|
69
|
+
|
|
70
|
+
if not use_stderr:
|
|
71
|
+
click.secho("OK", fg="green", err=use_stderr)
|
|
72
|
+
|
|
73
|
+
tools_data = [
|
|
74
|
+
{"name": name, "description": spec.description} for name, spec in tools.items()
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
if utils.maybe_print_as_json(opts, tools_data):
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
print_tools(tools)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@mcp_.command(name="list_groups")
|
|
84
|
+
@decorators.common_cli_config_options
|
|
85
|
+
@decorators.common_cli_output_options
|
|
86
|
+
@decorators.common_api_auth_options
|
|
87
|
+
@decorators.initialise_api
|
|
88
|
+
@decorators.initialise_mcp
|
|
89
|
+
@click.pass_context
|
|
90
|
+
def list_groups(ctx, opts, mcp_server: server.DynamicMCPServer):
|
|
91
|
+
"""
|
|
92
|
+
List available tool groups and the tools they contain
|
|
93
|
+
"""
|
|
94
|
+
use_stderr = utils.should_use_stderr(opts)
|
|
95
|
+
|
|
96
|
+
if not use_stderr:
|
|
97
|
+
click.echo("Getting list of tool groups ... ", nl=False, err=use_stderr)
|
|
98
|
+
|
|
99
|
+
with utils.maybe_spinner(opts):
|
|
100
|
+
groups = mcp_server.list_groups()
|
|
101
|
+
|
|
102
|
+
if not use_stderr:
|
|
103
|
+
click.secho("OK", fg="green", err=use_stderr)
|
|
104
|
+
|
|
105
|
+
groups_data = [{"name": name, "tools": tools} for name, tools in groups.items()]
|
|
106
|
+
|
|
107
|
+
if utils.maybe_print_as_json(opts, groups_data):
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
print_groups(groups)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def print_tools(tool_list: Dict[str, OpenAPITool]):
|
|
114
|
+
"""Print tools as a table or output in another format."""
|
|
115
|
+
|
|
116
|
+
headers = [
|
|
117
|
+
"Name",
|
|
118
|
+
"Description",
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
rows = []
|
|
122
|
+
for tool_name, tools_spec in tool_list.items():
|
|
123
|
+
rows.append(
|
|
124
|
+
[
|
|
125
|
+
click.style(tool_name, fg="cyan"),
|
|
126
|
+
click.style(tools_spec.description, fg="yellow"),
|
|
127
|
+
]
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if tool_list:
|
|
131
|
+
click.echo()
|
|
132
|
+
utils.pretty_print_table(headers, rows)
|
|
133
|
+
|
|
134
|
+
click.echo()
|
|
135
|
+
|
|
136
|
+
num_results = len(tool_list)
|
|
137
|
+
list_suffix = "tool%s visible" % ("s" if num_results != 1 else "")
|
|
138
|
+
utils.pretty_print_list_info(num_results=num_results, suffix=list_suffix)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def print_groups(group_list: Dict[str, List[str]]):
|
|
142
|
+
"""Print tool groups as a table or output in another format."""
|
|
143
|
+
|
|
144
|
+
headers = [
|
|
145
|
+
"Group Name",
|
|
146
|
+
"Tool Count",
|
|
147
|
+
"Sample Tools",
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
rows = []
|
|
151
|
+
for group_name, tools in group_list.items():
|
|
152
|
+
# Show first 3 tools as samples
|
|
153
|
+
sample_tools = ", ".join(tools[:3])
|
|
154
|
+
if len(tools) > 3:
|
|
155
|
+
sample_tools += f", ... (+{len(tools) - 3} more)"
|
|
156
|
+
|
|
157
|
+
rows.append(
|
|
158
|
+
[
|
|
159
|
+
click.style(group_name, fg="cyan"),
|
|
160
|
+
click.style(str(len(tools)), fg="yellow"),
|
|
161
|
+
click.style(sample_tools, fg="white"),
|
|
162
|
+
]
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if group_list:
|
|
166
|
+
click.echo()
|
|
167
|
+
utils.pretty_print_table(headers, rows)
|
|
168
|
+
|
|
169
|
+
click.echo()
|
|
170
|
+
|
|
171
|
+
num_results = len(group_list)
|
|
172
|
+
list_suffix = "group%s visible" % ("s" if num_results != 1 else "")
|
|
173
|
+
utils.pretty_print_list_info(num_results=num_results, suffix=list_suffix)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@mcp_.command(name="configure")
|
|
177
|
+
@decorators.common_cli_config_options
|
|
178
|
+
@decorators.common_cli_output_options
|
|
179
|
+
@decorators.common_api_auth_options
|
|
180
|
+
@click.option(
|
|
181
|
+
"--client",
|
|
182
|
+
type=click.Choice(list(SUPPORTED_MCP_CLIENTS.keys()), case_sensitive=False),
|
|
183
|
+
help=f"MCP client to configure ({', '.join(SUPPORTED_MCP_CLIENTS.keys())}). If not specified, will attempt to detect and configure all.",
|
|
184
|
+
)
|
|
185
|
+
@click.option(
|
|
186
|
+
"--global/--local",
|
|
187
|
+
"is_global",
|
|
188
|
+
default=True,
|
|
189
|
+
help="Configure globally (default) or in current project directory (local)",
|
|
190
|
+
)
|
|
191
|
+
@decorators.initialise_api
|
|
192
|
+
@click.pass_context
|
|
193
|
+
def configure(ctx, opts, client, is_global): # pylint: disable=unused-argument
|
|
194
|
+
"""
|
|
195
|
+
Configure the Cloudsmith MCP server for supported clients.
|
|
196
|
+
|
|
197
|
+
This command automatically adds the Cloudsmith MCP server configuration
|
|
198
|
+
to the specified client's configuration file. Supported clients are:
|
|
199
|
+
- Claude Desktop
|
|
200
|
+
- Cursor IDE
|
|
201
|
+
- VS Code (GitHub Copilot)
|
|
202
|
+
- Gemini CLI
|
|
203
|
+
|
|
204
|
+
Examples:\n
|
|
205
|
+
cloudsmith mcp configure --client claude\n
|
|
206
|
+
cloudsmith mcp configure --client cursor --local\n
|
|
207
|
+
cloudsmith mcp configure --client gemini-cli\n
|
|
208
|
+
cloudsmith mcp configure # Auto-detect and configure all
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
use_stderr = utils.should_use_stderr(opts)
|
|
212
|
+
|
|
213
|
+
# Get the profile from context
|
|
214
|
+
profile = ctx.meta.get("profile")
|
|
215
|
+
|
|
216
|
+
# Determine the best command to run the MCP server
|
|
217
|
+
server_config = _get_server_config(profile)
|
|
218
|
+
|
|
219
|
+
clients_to_configure = []
|
|
220
|
+
if client:
|
|
221
|
+
clients_to_configure = [client.lower()]
|
|
222
|
+
else:
|
|
223
|
+
# Auto-detect available clients
|
|
224
|
+
clients_to_configure = detect_available_clients()
|
|
225
|
+
|
|
226
|
+
if not clients_to_configure:
|
|
227
|
+
if not use_stderr:
|
|
228
|
+
click.echo(click.style("No supported MCP clients detected.", fg="yellow"))
|
|
229
|
+
click.echo("\nSupported clients:")
|
|
230
|
+
for display_name in SUPPORTED_MCP_CLIENTS.values():
|
|
231
|
+
click.echo(f" - {display_name}")
|
|
232
|
+
|
|
233
|
+
utils.maybe_print_as_json(opts, [])
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
results = []
|
|
237
|
+
success_count = 0
|
|
238
|
+
for client_name in clients_to_configure:
|
|
239
|
+
try:
|
|
240
|
+
if configure_client(client_name, server_config, is_global, profile):
|
|
241
|
+
if not use_stderr:
|
|
242
|
+
click.echo(
|
|
243
|
+
click.style(f"✓ Configured {client_name.title()}", fg="green")
|
|
244
|
+
)
|
|
245
|
+
success_count += 1
|
|
246
|
+
results.append({"client": client_name, "success": True})
|
|
247
|
+
else:
|
|
248
|
+
if not use_stderr:
|
|
249
|
+
click.echo(
|
|
250
|
+
click.style(
|
|
251
|
+
f"✗ Failed to configure {client_name.title()}", fg="red"
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
results.append(
|
|
255
|
+
{
|
|
256
|
+
"client": client_name,
|
|
257
|
+
"success": False,
|
|
258
|
+
"error": "Configuration failed",
|
|
259
|
+
}
|
|
260
|
+
)
|
|
261
|
+
except (OSError, ValueError) as e:
|
|
262
|
+
if not use_stderr:
|
|
263
|
+
click.echo(
|
|
264
|
+
click.style(
|
|
265
|
+
f"✗ Error configuring {client_name.title()}: {str(e)}", fg="red"
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
results.append({"client": client_name, "success": False, "error": str(e)})
|
|
269
|
+
|
|
270
|
+
if utils.maybe_print_as_json(opts, results):
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
if success_count > 0:
|
|
274
|
+
click.echo(
|
|
275
|
+
click.style(
|
|
276
|
+
f"\n✓ Successfully configured {success_count} client(s)", fg="green"
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
click.echo(
|
|
280
|
+
"\nNote: You may need to restart the client application for changes to take effect."
|
|
281
|
+
)
|
|
282
|
+
else:
|
|
283
|
+
click.echo(click.style("\n✗ No clients were configured successfully", fg="red"))
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _get_server_config(profile=None):
|
|
287
|
+
"""Determine the first available command configuration to run the MCP server."""
|
|
288
|
+
# Check if running in a virtual environment
|
|
289
|
+
in_venv = hasattr(sys, "real_prefix") or (
|
|
290
|
+
hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Build the base args
|
|
294
|
+
base_args = []
|
|
295
|
+
if profile:
|
|
296
|
+
base_args.extend(["-P", profile])
|
|
297
|
+
|
|
298
|
+
# In a venv, always use python -m to ensure we use the venv's packages
|
|
299
|
+
if in_venv:
|
|
300
|
+
return {
|
|
301
|
+
"command": sys.executable,
|
|
302
|
+
"args": ["-m", "cloudsmith_cli"] + base_args + ["mcp", "start"],
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
# Otherwise, try to find cloudsmith in PATH, fall back to python -m
|
|
306
|
+
cloudsmith_cmd = shutil.which("cloudsmith")
|
|
307
|
+
if cloudsmith_cmd:
|
|
308
|
+
return {"command": cloudsmith_cmd, "args": base_args + ["mcp", "start"]}
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
"command": sys.executable,
|
|
312
|
+
"args": ["-m", "cloudsmith_cli"] + base_args + ["mcp", "start"],
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def detect_available_clients():
|
|
317
|
+
"""Detect which MCP clients are available on the system."""
|
|
318
|
+
available = []
|
|
319
|
+
|
|
320
|
+
for client in SUPPORTED_MCP_CLIENTS:
|
|
321
|
+
config = get_config_path(client, is_global=True)
|
|
322
|
+
if config and config.parent.exists():
|
|
323
|
+
available.append(client)
|
|
324
|
+
|
|
325
|
+
return available
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def get_config_path(client_name, is_global=True):
|
|
329
|
+
"""Get the configuration file path for a given client."""
|
|
330
|
+
home = Path.home()
|
|
331
|
+
appdata = os.getenv("APPDATA", "")
|
|
332
|
+
|
|
333
|
+
# Configuration paths by client, platform, and scope
|
|
334
|
+
config_paths = {
|
|
335
|
+
"claude": {
|
|
336
|
+
"darwin": home
|
|
337
|
+
/ "Library"
|
|
338
|
+
/ "Application Support"
|
|
339
|
+
/ "Claude"
|
|
340
|
+
/ "claude_desktop_config.json",
|
|
341
|
+
"win32": (
|
|
342
|
+
Path(appdata) / "Claude" / "claude_desktop_config.json"
|
|
343
|
+
if appdata
|
|
344
|
+
else None
|
|
345
|
+
),
|
|
346
|
+
"linux": home / ".config" / "Claude" / "claude_desktop_config.json",
|
|
347
|
+
},
|
|
348
|
+
"cursor": {
|
|
349
|
+
"global": home / ".cursor" / "mcp.json",
|
|
350
|
+
"local": Path.cwd() / ".cursor" / "mcp.json",
|
|
351
|
+
},
|
|
352
|
+
"vscode": {
|
|
353
|
+
"darwin": home
|
|
354
|
+
/ "Library"
|
|
355
|
+
/ "Application Support"
|
|
356
|
+
/ "Code"
|
|
357
|
+
/ "User"
|
|
358
|
+
/ "settings.json",
|
|
359
|
+
"win32": (
|
|
360
|
+
Path(appdata) / "Code" / "User" / "settings.json" if appdata else None
|
|
361
|
+
),
|
|
362
|
+
"linux": home / ".config" / "Code" / "User" / "settings.json",
|
|
363
|
+
"local": Path.cwd() / ".vscode" / "settings.json",
|
|
364
|
+
},
|
|
365
|
+
"gemini-cli": {
|
|
366
|
+
"global": home / ".gemini" / "settings.json",
|
|
367
|
+
"local": Path.cwd() / ".gemini" / "settings.json",
|
|
368
|
+
},
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
client_config = config_paths.get(client_name, {})
|
|
372
|
+
|
|
373
|
+
# For Cursor and Gemini CLI, use global/local scope instead of platform
|
|
374
|
+
if client_name in ("cursor", "gemini-cli"):
|
|
375
|
+
scope = "global" if is_global else "local"
|
|
376
|
+
return client_config.get(scope)
|
|
377
|
+
|
|
378
|
+
# For VS Code local config
|
|
379
|
+
if client_name == "vscode" and not is_global:
|
|
380
|
+
return client_config.get("local")
|
|
381
|
+
|
|
382
|
+
# For platform-specific configs (Claude and VS Code global)
|
|
383
|
+
platform = sys.platform if sys.platform in ("darwin", "win32") else "linux"
|
|
384
|
+
return client_config.get(platform)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def configure_client(client_name, server_config, is_global=True, profile=None):
|
|
388
|
+
"""Configure a specific MCP client with the Cloudsmith server."""
|
|
389
|
+
config_path = get_config_path(client_name, is_global)
|
|
390
|
+
|
|
391
|
+
if not config_path:
|
|
392
|
+
return False
|
|
393
|
+
|
|
394
|
+
# Ensure parent directory exists
|
|
395
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
396
|
+
|
|
397
|
+
# Read existing config or create new one
|
|
398
|
+
config = {}
|
|
399
|
+
if config_path.exists():
|
|
400
|
+
with open(config_path) as f:
|
|
401
|
+
content = f.read()
|
|
402
|
+
try:
|
|
403
|
+
# Use json5 first as it handles both standard JSON and JSONC
|
|
404
|
+
# (comments, trailing commas) which VS Code settings.json may contain
|
|
405
|
+
config = json5.loads(content)
|
|
406
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
407
|
+
raise ValueError(
|
|
408
|
+
f"Cannot parse config file '{config_path}': {e}. "
|
|
409
|
+
f"Please fix the JSON syntax or remove the file to create a new one."
|
|
410
|
+
) from e
|
|
411
|
+
|
|
412
|
+
# Determine server name based on profile
|
|
413
|
+
server_name = f"cloudsmith-{profile}" if profile else "cloudsmith"
|
|
414
|
+
|
|
415
|
+
# Add Cloudsmith MCP server based on client format
|
|
416
|
+
if client_name in {"claude", "cursor", "gemini-cli"}:
|
|
417
|
+
if "mcpServers" not in config:
|
|
418
|
+
config["mcpServers"] = {}
|
|
419
|
+
config["mcpServers"][server_name] = server_config
|
|
420
|
+
|
|
421
|
+
elif client_name == "vscode":
|
|
422
|
+
# VS Code uses a different format in settings.json
|
|
423
|
+
if "chat.mcp.servers" not in config:
|
|
424
|
+
config["chat.mcp.servers"] = {}
|
|
425
|
+
config["chat.mcp.servers"][server_name] = server_config
|
|
426
|
+
|
|
427
|
+
# Write updated config
|
|
428
|
+
with open(config_path, "w") as f:
|
|
429
|
+
# As we are using json5 to read, we write standard JSON back, which removes existing user JSON comments in the files (currently only present in VS Code settings.json).
|
|
430
|
+
json.dump(config, f, indent=2)
|
|
431
|
+
|
|
432
|
+
return True
|
|
@@ -75,7 +75,7 @@ def _print_metrics_table(opts, data):
|
|
|
75
75
|
type=str,
|
|
76
76
|
required=False,
|
|
77
77
|
help=(
|
|
78
|
-
"A comma
|
|
78
|
+
"A comma separated list of entitlement token identifiers (i.e. slug_perm) or "
|
|
79
79
|
"token secrets. If a list is not specified then all entitlement tokens will "
|
|
80
80
|
"be included for a given namespace or repository."
|
|
81
81
|
),
|
|
@@ -74,7 +74,7 @@ def _print_metrics_table(opts, data):
|
|
|
74
74
|
type=str,
|
|
75
75
|
required=False,
|
|
76
76
|
help=(
|
|
77
|
-
"A comma
|
|
77
|
+
"A comma separated list of package identifiers (i.e. slug_perm). "
|
|
78
78
|
"If a list is not specified then all package will be included for "
|
|
79
79
|
"a given repository."
|
|
80
80
|
),
|
cloudsmith_cli/cli/config.py
CHANGED
|
@@ -64,6 +64,8 @@ class ConfigSchema:
|
|
|
64
64
|
api_proxy = ConfigParam(name="api_proxy", type=str)
|
|
65
65
|
api_ssl_verify = ConfigParam(name="api_ssl_verify", type=bool, default=True)
|
|
66
66
|
api_user_agent = ConfigParam(name="api_user_agent", type=str)
|
|
67
|
+
mcp_allowed_tools = ConfigParam(name="mcp_allowed_tools", type=str)
|
|
68
|
+
mcp_allowed_tool_groups = ConfigParam(name="mcp_allowed_tool_groups", type=str)
|
|
67
69
|
|
|
68
70
|
@matches_section("profile:*")
|
|
69
71
|
class Profile(Default):
|
|
@@ -95,7 +97,7 @@ class ConfigReader(ConfigFileReader):
|
|
|
95
97
|
|
|
96
98
|
@classmethod
|
|
97
99
|
def get_default_filepath(cls):
|
|
98
|
-
"""Get the default filepath for the
|
|
100
|
+
"""Get the default filepath for the configuration file."""
|
|
99
101
|
if not cls.config_files:
|
|
100
102
|
return None
|
|
101
103
|
if not cls.config_searchpath:
|
|
@@ -348,6 +350,34 @@ class Options:
|
|
|
348
350
|
"""Set value for debug flag."""
|
|
349
351
|
self._set_option("debug", bool(value))
|
|
350
352
|
|
|
353
|
+
@property
|
|
354
|
+
def mcp_allowed_tools(self):
|
|
355
|
+
"""Get value for Allowed MCP Tools."""
|
|
356
|
+
return self._get_option("mcp_allowed_tools")
|
|
357
|
+
|
|
358
|
+
@mcp_allowed_tools.setter
|
|
359
|
+
def mcp_allowed_tools(self, value):
|
|
360
|
+
"""Set value for Allowed MCP Tools."""
|
|
361
|
+
if not value:
|
|
362
|
+
return
|
|
363
|
+
tools = [tool.strip() for tool in value.split(",")]
|
|
364
|
+
|
|
365
|
+
self._set_option("mcp_allowed_tools", tools)
|
|
366
|
+
|
|
367
|
+
@property
|
|
368
|
+
def mcp_allowed_tool_groups(self):
|
|
369
|
+
"""Get value for Allowed MCP Tool Groups."""
|
|
370
|
+
return self._get_option("mcp_allowed_tool_groups")
|
|
371
|
+
|
|
372
|
+
@mcp_allowed_tool_groups.setter
|
|
373
|
+
def mcp_allowed_tool_groups(self, value):
|
|
374
|
+
"""Set value for Allowed MCP Tool Groups."""
|
|
375
|
+
if not value:
|
|
376
|
+
return
|
|
377
|
+
tool_groups = [group.strip() for group in value.split(",")]
|
|
378
|
+
|
|
379
|
+
self._set_option("mcp_allowed_tool_groups", tool_groups)
|
|
380
|
+
|
|
351
381
|
@property
|
|
352
382
|
def output(self):
|
|
353
383
|
"""Get value for output format."""
|
cloudsmith_cli/cli/decorators.py
CHANGED
|
@@ -7,6 +7,7 @@ import click
|
|
|
7
7
|
from cloudsmith_cli.cli import validators
|
|
8
8
|
|
|
9
9
|
from ..core.api.init import initialise_api as _initialise_api
|
|
10
|
+
from ..core.mcp import server
|
|
10
11
|
from . import config, utils
|
|
11
12
|
|
|
12
13
|
|
|
@@ -93,9 +94,15 @@ def common_cli_config_options(f):
|
|
|
93
94
|
def wrapper(ctx, *args, **kwargs):
|
|
94
95
|
# pylint: disable=missing-docstring
|
|
95
96
|
opts = config.get_or_create_options(ctx)
|
|
96
|
-
profile = kwargs.pop("profile")
|
|
97
|
-
config_file = kwargs.pop("config_file")
|
|
98
|
-
creds_file = kwargs.pop("credentials_file")
|
|
97
|
+
profile = kwargs.pop("profile") or ctx.meta.get("profile")
|
|
98
|
+
config_file = kwargs.pop("config_file") or ctx.meta.get("config_file")
|
|
99
|
+
creds_file = kwargs.pop("credentials_file") or ctx.meta.get("creds_file")
|
|
100
|
+
|
|
101
|
+
# Store in context for subcommands to inherit
|
|
102
|
+
ctx.meta["profile"] = profile
|
|
103
|
+
ctx.meta["config_file"] = config_file
|
|
104
|
+
ctx.meta["creds_file"] = creds_file
|
|
105
|
+
|
|
99
106
|
opts.load_config_file(path=config_file, profile=profile)
|
|
100
107
|
opts.load_creds_file(path=creds_file, profile=profile)
|
|
101
108
|
kwargs["opts"] = opts
|
|
@@ -330,3 +337,40 @@ def initialise_api(f):
|
|
|
330
337
|
return ctx.invoke(f, *args, **kwargs)
|
|
331
338
|
|
|
332
339
|
return wrapper
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def initialise_mcp(f):
|
|
343
|
+
@click.option(
|
|
344
|
+
"-a",
|
|
345
|
+
"--all-tools",
|
|
346
|
+
default=False,
|
|
347
|
+
is_flag=True,
|
|
348
|
+
help="Show all tools",
|
|
349
|
+
)
|
|
350
|
+
@click.option(
|
|
351
|
+
"-D",
|
|
352
|
+
"--allow-destructive-tools",
|
|
353
|
+
default=False,
|
|
354
|
+
is_flag=True,
|
|
355
|
+
help="Allow destructive tools to be used",
|
|
356
|
+
)
|
|
357
|
+
@click.pass_context
|
|
358
|
+
@functools.wraps(f)
|
|
359
|
+
def wrapper(ctx, *args, **kwargs):
|
|
360
|
+
opts = kwargs.get("opts")
|
|
361
|
+
|
|
362
|
+
all_tools = kwargs.pop("all_tools")
|
|
363
|
+
allow_destructive_tools = kwargs.pop("allow_destructive_tools")
|
|
364
|
+
|
|
365
|
+
mcp_server = server.DynamicMCPServer(
|
|
366
|
+
api_config=opts.api_config,
|
|
367
|
+
debug_mode=opts.debug,
|
|
368
|
+
allow_destructive_tools=allow_destructive_tools,
|
|
369
|
+
allowed_tool_groups=opts.mcp_allowed_tool_groups,
|
|
370
|
+
allowed_tools=opts.mcp_allowed_tools,
|
|
371
|
+
force_all_tools=all_tools,
|
|
372
|
+
)
|
|
373
|
+
kwargs["mcp_server"] = mcp_server
|
|
374
|
+
return ctx.invoke(f, *args, **kwargs)
|
|
375
|
+
|
|
376
|
+
return wrapper
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
1
3
|
import pytest
|
|
2
4
|
|
|
3
5
|
from ....core.api.version import get_version as get_api_version
|
|
@@ -17,6 +19,32 @@ class TestMainCommand:
|
|
|
17
19
|
"API Package Version: " + get_api_version() + "\n"
|
|
18
20
|
)
|
|
19
21
|
|
|
22
|
+
@pytest.mark.parametrize("option", ["-V", "--version"])
|
|
23
|
+
@pytest.mark.parametrize(
|
|
24
|
+
"format_option,format_value",
|
|
25
|
+
[("-F", "json"), ("--output-format", "json")],
|
|
26
|
+
)
|
|
27
|
+
def test_main_version_json(self, runner, option, format_option, format_value):
|
|
28
|
+
"""Test the JSON output of `cloudsmith --version --output-format json`."""
|
|
29
|
+
result = runner.invoke(main, [option, format_option, format_value])
|
|
30
|
+
assert result.exit_code == 0
|
|
31
|
+
output = json.loads(result.output)
|
|
32
|
+
assert "data" in output
|
|
33
|
+
assert output["data"]["cli_version"] == get_version()
|
|
34
|
+
assert output["data"]["api_version"] == get_api_version()
|
|
35
|
+
|
|
36
|
+
@pytest.mark.parametrize("option", ["-V", "--version"])
|
|
37
|
+
def test_main_version_pretty_json(self, runner, option):
|
|
38
|
+
"""Test the pretty JSON output of `cloudsmith --version --output-format pretty_json`."""
|
|
39
|
+
result = runner.invoke(main, [option, "--output-format", "pretty_json"])
|
|
40
|
+
assert result.exit_code == 0
|
|
41
|
+
output = json.loads(result.output)
|
|
42
|
+
assert "data" in output
|
|
43
|
+
assert output["data"]["cli_version"] == get_version()
|
|
44
|
+
assert output["data"]["api_version"] == get_api_version()
|
|
45
|
+
# Verify it's formatted with indentation
|
|
46
|
+
assert " " in result.output
|
|
47
|
+
|
|
20
48
|
@pytest.mark.parametrize("option", ["-h", "--help"])
|
|
21
49
|
def test_main_help(self, runner, option):
|
|
22
50
|
"""Test the output of `cloudsmith --help`."""
|