qbraid-cli 0.8.5a1__py3-none-any.whl → 0.12.0a0__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 (38) hide show
  1. qbraid_cli/_version.py +16 -14
  2. qbraid_cli/account/__init__.py +11 -0
  3. qbraid_cli/account/app.py +67 -0
  4. qbraid_cli/admin/app.py +21 -13
  5. qbraid_cli/admin/headers.py +132 -23
  6. qbraid_cli/admin/validation.py +1 -8
  7. qbraid_cli/chat/__init__.py +11 -0
  8. qbraid_cli/chat/app.py +76 -0
  9. qbraid_cli/configure/actions.py +21 -2
  10. qbraid_cli/configure/app.py +147 -2
  11. qbraid_cli/configure/claude_config.py +215 -0
  12. qbraid_cli/devices/app.py +27 -4
  13. qbraid_cli/envs/activate.py +38 -8
  14. qbraid_cli/envs/app.py +716 -89
  15. qbraid_cli/envs/create.py +3 -2
  16. qbraid_cli/files/__init__.py +11 -0
  17. qbraid_cli/files/app.py +139 -0
  18. qbraid_cli/handlers.py +35 -5
  19. qbraid_cli/jobs/app.py +33 -13
  20. qbraid_cli/jobs/toggle_braket.py +2 -13
  21. qbraid_cli/jobs/validation.py +1 -0
  22. qbraid_cli/kernels/app.py +4 -3
  23. qbraid_cli/main.py +57 -13
  24. qbraid_cli/mcp/__init__.py +10 -0
  25. qbraid_cli/mcp/app.py +126 -0
  26. qbraid_cli/mcp/serve.py +321 -0
  27. qbraid_cli/pip/app.py +2 -2
  28. qbraid_cli/pip/hooks.py +1 -0
  29. {qbraid_cli-0.8.5a1.dist-info → qbraid_cli-0.12.0a0.dist-info}/METADATA +37 -14
  30. qbraid_cli-0.12.0a0.dist-info/RECORD +46 -0
  31. {qbraid_cli-0.8.5a1.dist-info → qbraid_cli-0.12.0a0.dist-info}/WHEEL +1 -1
  32. {qbraid_cli-0.8.5a1.dist-info → qbraid_cli-0.12.0a0.dist-info/licenses}/LICENSE +6 -4
  33. qbraid_cli/admin/buildlogs.py +0 -114
  34. qbraid_cli/credits/__init__.py +0 -11
  35. qbraid_cli/credits/app.py +0 -35
  36. qbraid_cli-0.8.5a1.dist-info/RECORD +0 -39
  37. {qbraid_cli-0.8.5a1.dist-info → qbraid_cli-0.12.0a0.dist-info}/entry_points.txt +0 -0
  38. {qbraid_cli-0.8.5a1.dist-info → qbraid_cli-0.12.0a0.dist-info}/top_level.txt +0 -0
qbraid_cli/envs/create.py CHANGED
@@ -5,6 +5,7 @@
5
5
  Module supporting 'qbraid envs create' command.
6
6
 
7
7
  """
8
+ from qbraid_core.services.environments.schema import EnvironmentConfig
8
9
 
9
10
 
10
11
  def create_venv(*args, **kwargs) -> None:
@@ -21,9 +22,9 @@ def update_state_json(*ags, **kwargs) -> None:
21
22
  return update_state(*ags, **kwargs)
22
23
 
23
24
 
24
- def create_qbraid_env_assets(slug: str, alias: str, kernel_name: str, slug_path: str) -> None:
25
+ def create_qbraid_env_assets(env_id: str, env_id_path: str, env_config: EnvironmentConfig) -> None:
25
26
  """Create a qBraid environment including python venv, PS1 configs,
26
27
  kernel resource files, and qBraid state.json."""
27
28
  from qbraid_core.services.environments.create import create_qbraid_env_assets as create_assets
28
29
 
29
- return create_assets(slug, alias, kernel_name, slug_path)
30
+ return create_assets(env_id, env_id_path, env_config)
@@ -0,0 +1,11 @@
1
+ # Copyright (c) 2024, qBraid Development Team
2
+ # All rights reserved.
3
+
4
+ """
5
+ Module defining the qbraid files namespace
6
+
7
+ """
8
+
9
+ from .app import files_app
10
+
11
+ __all__ = ["files_app"]
@@ -0,0 +1,139 @@
1
+ # Copyright (c) 2024, qBraid Development Team
2
+ # All rights reserved.
3
+
4
+ """
5
+ Module defining commands in the 'qbraid files' namespace.
6
+
7
+ """
8
+
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ import rich
13
+ import typer
14
+
15
+ from qbraid_cli.handlers import handle_error, run_progress_task
16
+
17
+ files_app = typer.Typer(help="Manage qBraid cloud storage files.", no_args_is_help=True)
18
+
19
+
20
+ def is_file_less_than_10mb(file_path: Path) -> bool:
21
+ """
22
+ Check if the given file is less than 10MB in size.
23
+
24
+ Args:
25
+ file_path (Path): The path to the file to check.
26
+
27
+ Returns:
28
+ bool: True if the file is less than 10MB, False otherwise.
29
+ """
30
+ ten_mb = 10485760 # 10 MB in bytes (10 * 1024 * 1024)
31
+
32
+ try:
33
+ return file_path.stat().st_size < ten_mb
34
+ except OSError:
35
+ return False
36
+
37
+
38
+ @files_app.command(name="upload")
39
+ def files_upload(
40
+ filepath: Path = typer.Argument(
41
+ ...,
42
+ exists=True,
43
+ dir_okay=False,
44
+ resolve_path=True,
45
+ help="Local path to the file to upload.",
46
+ ),
47
+ namespace: str = typer.Option(
48
+ "user",
49
+ "--namespace",
50
+ "-n",
51
+ help="Target qBraid namespace for the upload.",
52
+ ),
53
+ object_path: str = typer.Option(
54
+ None,
55
+ "--object-path",
56
+ "-p",
57
+ help=("Target object path. " "Defaults to original filename in namespace root."),
58
+ ),
59
+ overwrite: bool = typer.Option(
60
+ False,
61
+ "--overwrite",
62
+ "-o",
63
+ help="Overwrite existing file if it already exists in the target location.",
64
+ ),
65
+ ):
66
+ """Upload a local file to qBraid storage."""
67
+
68
+ if not is_file_less_than_10mb(filepath):
69
+ handle_error("Error", "File too large. Must be less than 10MB for direct upload.")
70
+
71
+ def upload_file() -> dict[str, Any]:
72
+ from qbraid_core.services.storage import FileStorageClient
73
+
74
+ client = FileStorageClient()
75
+ data = client.upload_file(
76
+ filepath, namespace=namespace, object_path=object_path, overwrite=overwrite
77
+ )
78
+ return data
79
+
80
+ data: dict = run_progress_task(
81
+ upload_file, description="Uploading file...", include_error_traceback=False
82
+ )
83
+
84
+ rich.print("File uploaded successfully!")
85
+ namespace = data.get("namespace")
86
+ object_path = data.get("objectPath")
87
+
88
+ if namespace and object_path:
89
+ rich.print(f"\nNamespace: '{namespace}'")
90
+ rich.print(f"Object path: '{object_path}'")
91
+
92
+
93
+ @files_app.command(name="download")
94
+ def files_download(
95
+ object_path: str = typer.Argument(
96
+ ...,
97
+ help="The folder + filename describing the file to download.",
98
+ ),
99
+ namespace: str = typer.Option(
100
+ "user",
101
+ "--namespace",
102
+ "-n",
103
+ help="Source qBraid namespace for the download.",
104
+ ),
105
+ save_path: Path = typer.Option(
106
+ Path.cwd(),
107
+ "--save-path",
108
+ "-s",
109
+ resolve_path=True,
110
+ help="Local directory to save the downloaded file.",
111
+ ),
112
+ overwrite: bool = typer.Option(
113
+ False,
114
+ "--overwrite",
115
+ "-o",
116
+ help="Overwrite existing file if it already exists in the target location.",
117
+ ),
118
+ ):
119
+ """Download a file from qBraid storage."""
120
+
121
+ def download_file() -> Path:
122
+ from qbraid_core.services.storage import FileStorageClient
123
+
124
+ client = FileStorageClient()
125
+ file_path = client.download_file(
126
+ object_path, namespace=namespace, save_path=save_path, overwrite=overwrite
127
+ )
128
+ return file_path
129
+
130
+ file_path: Path = run_progress_task(
131
+ download_file, description="Downloading file...", include_error_traceback=False
132
+ )
133
+
134
+ rich.print("File downloaded successfully!")
135
+ rich.print(f"Saved to: '{str(file_path)}'")
136
+
137
+
138
+ if __name__ == "__main__":
139
+ files_app()
qbraid_cli/handlers.py CHANGED
@@ -10,11 +10,12 @@ and executing operations with progress tracking within the qBraid CLI.
10
10
  import os
11
11
  import traceback
12
12
  from pathlib import Path
13
+ from time import sleep
13
14
  from typing import Any, Callable, Optional, Union
14
15
 
15
16
  import typer
16
17
  from rich.console import Console
17
- from rich.progress import Progress, SpinnerColumn, TextColumn
18
+ from rich.progress import Progress, SpinnerColumn, TaskID, TextColumn
18
19
 
19
20
  from .exceptions import DEFAULT_ERROR_MESSAGE, QbraidException
20
21
 
@@ -24,6 +25,14 @@ def _should_display_progress():
24
25
  return os.getenv("QBRAID_CLI_SHOW_PROGRESS", "true").lower() in ["true", "1", "t", "y", "yes"]
25
26
 
26
27
 
28
+ def _update_completed_task(
29
+ progress: Progress, task_id: TaskID, success: bool = True, sleep_time: float = 0.15
30
+ ):
31
+ status = "Done" if success else "Failed"
32
+ progress.update(task_id, completed=100, status=status)
33
+ sleep(sleep_time)
34
+
35
+
27
36
  def handle_error(
28
37
  error_type: Optional[str] = None, message: Optional[str] = None, include_traceback: bool = True
29
38
  ) -> None:
@@ -80,11 +89,28 @@ def handle_filesystem_operation(operation: Callable[[], None], path: Path) -> No
80
89
  raise QbraidException(f"Failed to save configuration to {path}: {err.strerror}") from err
81
90
 
82
91
 
92
+ def print_formatted_data(data: Any, fmt: bool = True) -> None:
93
+ """
94
+ Print data with optional formatting using rich console.
95
+
96
+ Args:
97
+ data (Any): The data to be printed.
98
+ fmt (bool): If True, use rich console formatting. If False, use standard print.
99
+ Defaults to True.
100
+ """
101
+ if fmt:
102
+ console = Console()
103
+ console.print(data)
104
+ else:
105
+ print(data)
106
+
107
+
83
108
  def run_progress_task(
84
109
  operation: Callable[..., Any],
85
110
  *args,
86
111
  description: Optional[str] = None,
87
112
  error_message: Optional[str] = None,
113
+ include_error_traceback: bool = True,
88
114
  **kwargs,
89
115
  ) -> Any:
90
116
  """
@@ -102,6 +128,8 @@ def run_progress_task(
102
128
  error_message (optional, str): Custom error message to display if the operation.
103
129
  fails. Defaults to None, in which case the
104
130
  exception's message is used.
131
+ include_error_traceback (bool): Whether to include the traceback in the error message.
132
+ Defaults to True.
105
133
  **kwargs: Arbitrary keyword arguments for the operation.
106
134
 
107
135
  Returns:
@@ -115,7 +143,7 @@ def run_progress_task(
115
143
  return operation(*args, **kwargs)
116
144
  except Exception as err: # pylint: disable=broad-exception-caught
117
145
  custom_message = error_message if error_message else str(err)
118
- return handle_error(message=custom_message)
146
+ return handle_error(message=custom_message, include_traceback=include_error_traceback)
119
147
 
120
148
  console = Console()
121
149
  with Progress(
@@ -128,12 +156,14 @@ def run_progress_task(
128
156
  task = progress.add_task(description, status="In Progress", total=None)
129
157
  try:
130
158
  result = operation(*args, **kwargs)
131
- progress.update(task, completed=100, status="Done")
159
+ _update_completed_task(progress, task, success=True)
132
160
  return result
133
161
  except Exception as err: # pylint: disable=broad-exception-caught
134
- progress.update(task, completed=100, status="Failed")
162
+ _update_completed_task(progress, task, success=False)
135
163
  custom_message = error_message if error_message else str(err)
136
- return handle_error(message=custom_message)
164
+ return handle_error(message=custom_message, include_traceback=include_error_traceback)
165
+ finally:
166
+ progress.remove_task(task)
137
167
 
138
168
 
139
169
  def _format_list_items(items: list[str]) -> str:
qbraid_cli/jobs/app.py CHANGED
@@ -5,16 +5,17 @@
5
5
  Module defining commands in the 'qbraid jobs' namespace.
6
6
 
7
7
  """
8
+
8
9
  from typing import Any, Callable
9
10
 
10
11
  import typer
11
12
  from rich.console import Console
12
13
 
13
- from qbraid_cli.handlers import handle_error, run_progress_task
14
+ from qbraid_cli.handlers import handle_error, print_formatted_data, run_progress_task
14
15
  from qbraid_cli.jobs.toggle_braket import disable_braket, enable_braket
15
16
  from qbraid_cli.jobs.validation import handle_jobs_state, run_progress_get_state, validate_library
16
17
 
17
- jobs_app = typer.Typer(help="Manage qBraid quantum jobs.")
18
+ jobs_app = typer.Typer(help="Manage qBraid quantum jobs.", no_args_is_help=True)
18
19
 
19
20
 
20
21
  @jobs_app.command(name="enable")
@@ -63,7 +64,7 @@ def jobs_state(
63
64
  default=None,
64
65
  help="Optional: Specify a software library with quantum jobs support to check its status.",
65
66
  callback=validate_library,
66
- )
67
+ ),
67
68
  ) -> None:
68
69
  """Display the state of qBraid Quantum Jobs for the current environment."""
69
70
  result: tuple[str, dict[str, tuple[bool, bool]]] = run_progress_get_state(library)
@@ -75,23 +76,25 @@ def jobs_state(
75
76
  max_lib_length = max((len(lib) for lib in state_values.keys()), default=len(header_1))
76
77
  padding = max_lib_length + 9
77
78
 
78
- console.print(f"Executable: {python_exe}")
79
- console.print(f"\n{header_1:<{padding}}{header_2}", style="bold")
80
-
79
+ output = ""
81
80
  for lib, (installed, enabled) in state_values.items():
82
81
  state_str = (
83
82
  "[green]enabled"
84
83
  if enabled and installed
85
84
  else "[red]disabled" if installed else "[grey70]unavailable"
86
85
  )
87
- console.print(f"{lib:<{padding-1}}", state_str, end="\n")
86
+ output += f"{lib:<{padding-1}} {state_str}\n"
87
+
88
+ console.print(f"Executable: {python_exe}")
89
+ console.print(f"\n{header_1:<{padding}}{header_2}", style="bold")
90
+ console.print(output)
88
91
 
89
92
 
90
93
  @jobs_app.command(name="list")
91
94
  def jobs_list(
92
95
  limit: int = typer.Option(
93
96
  10, "--limit", "-l", help="Limit the maximum number of results returned"
94
- )
97
+ ),
95
98
  ) -> None:
96
99
  """List qBraid Quantum Jobs."""
97
100
 
@@ -104,11 +107,8 @@ def jobs_list(
104
107
 
105
108
  result: tuple[Any, Callable] = run_progress_task(import_jobs)
106
109
  client, process_job_data = result
107
- # https://github.com/qBraid/api/issues/644
108
- # raw_data = client.search_jobs(query={"numResults": limit})
109
- raw_data = client.search_jobs(query={})
110
+ raw_data = client.search_jobs(query={"resultsPerPage": limit, "page": 0})
110
111
  job_data, msg = process_job_data(raw_data)
111
- job_data = job_data[:limit]
112
112
 
113
113
  longest_job_id = max(len(item[0]) for item in job_data)
114
114
  spacing = longest_job_id + 5
@@ -117,7 +117,7 @@ def jobs_list(
117
117
  header_1 = "Job ID"
118
118
  header_2 = "Submitted"
119
119
  header_3 = "Status"
120
- console.print(f"\n[bold]{header_1.ljust(spacing)}{header_2.ljust(36)}{header_3}[/bold]")
120
+ console.print(f"[bold]{header_1.ljust(spacing)}{header_2.ljust(36)}{header_3}[/bold]")
121
121
  for job_id, submitted, status in job_data:
122
122
  if status == "COMPLETED":
123
123
  status_color = "green"
@@ -145,5 +145,25 @@ def jobs_list(
145
145
  handle_error(message="Failed to fetch quantum jobs.")
146
146
 
147
147
 
148
+ @jobs_app.command(name="get")
149
+ def jobs_get(
150
+ job_id: str = typer.Argument(..., help="The ID of the job to get."),
151
+ fmt: bool = typer.Option(
152
+ True, "--no-fmt", help="Disable rich console formatting (output raw data)"
153
+ ),
154
+ ) -> None:
155
+ """Get a qBraid Quantum Job."""
156
+
157
+ def get_job():
158
+ from qbraid_core.services.quantum import QuantumClient
159
+
160
+ client = QuantumClient()
161
+ return client.get_job(job_id)
162
+
163
+ data: dict[str, Any] = run_progress_task(get_job)
164
+
165
+ print_formatted_data(data, fmt)
166
+
167
+
148
168
  if __name__ == "__main__":
149
169
  jobs_app()
@@ -5,6 +5,7 @@
5
5
  Module supporting 'qbraid jobs enable/disable braket' and commands.
6
6
 
7
7
  """
8
+
8
9
  import logging
9
10
  import os
10
11
  import re
@@ -102,7 +103,7 @@ def confirm_updates(
102
103
  else:
103
104
  raise ValueError(f"Invalid mode: {mode}. Expected 'enable' or 'disable'.")
104
105
 
105
- typer.echo(f"\n==> WARNING: {provider}/{core_package} package required <==")
106
+ typer.echo(f"==> WARNING: {provider}/{core_package} package required <==")
106
107
  if (
107
108
  installed_version is not None
108
109
  and target_version is not None
@@ -157,18 +158,6 @@ def enable_braket(auto_confirm: bool = False):
157
158
  aws_configure_dummy() # TODO: possibly add another confirmation for writing aws config files
158
159
 
159
160
  try:
160
- subprocess.check_call(
161
- [
162
- python_exe,
163
- "-m",
164
- "pip",
165
- "install",
166
- "amazon-braket-sdk",
167
- "--upgrade",
168
- "--upgrade-strategy",
169
- "eager",
170
- ]
171
- )
172
161
  subprocess.check_call([python_exe, "-m", "pip", "install", f"boto3=={target}"])
173
162
  subprocess.check_call([python_exe, "-m", "pip", "uninstall", "botocore", "-y", "--quiet"])
174
163
  subprocess.check_call(
@@ -5,6 +5,7 @@
5
5
  Module for validating command arguments for qBraid Quantum Jobs.
6
6
 
7
7
  """
8
+
8
9
  import sys
9
10
  from typing import Any, Callable, Optional
10
11
 
qbraid_cli/kernels/app.py CHANGED
@@ -5,12 +5,13 @@
5
5
  Module defining commands in the 'qbraid kernels' namespace.
6
6
 
7
7
  """
8
+
8
9
  import typer
9
10
  from rich.console import Console
10
11
 
11
12
  from qbraid_cli.handlers import handle_error
12
13
 
13
- kernels_app = typer.Typer(help="Manage qBraid kernels.")
14
+ kernels_app = typer.Typer(help="Manage qBraid kernels.", no_args_is_help=True)
14
15
 
15
16
 
16
17
  @kernels_app.command(name="list")
@@ -55,7 +56,7 @@ def kernels_list():
55
56
  def kernels_add(
56
57
  environment: str = typer.Argument(
57
58
  ..., help="Name of environment for which to add ipykernel. Values from 'qbraid envs list'."
58
- )
59
+ ),
59
60
  ):
60
61
  """Add a kernel."""
61
62
  from qbraid_core.services.environments.kernels import add_kernels
@@ -77,7 +78,7 @@ def kernels_remove(
77
78
  environment: str = typer.Argument(
78
79
  ...,
79
80
  help=("Name of environment for which to remove ipykernel. Values from 'qbraid envs list'."),
80
- )
81
+ ),
81
82
  ):
82
83
  """Remove a kernel."""
83
84
  from qbraid_core.services.environments.kernels import remove_kernels
qbraid_cli/main.py CHANGED
@@ -6,27 +6,42 @@ Entrypoint for the qBraid CLI.
6
6
 
7
7
  """
8
8
 
9
+ import click
10
+ import rich
9
11
  import typer
10
12
 
11
- from qbraid_cli.admin.app import admin_app
12
- from qbraid_cli.configure.app import configure_app
13
- from qbraid_cli.credits.app import credits_app
14
- from qbraid_cli.devices.app import devices_app
15
- from qbraid_cli.envs.app import envs_app
16
- from qbraid_cli.jobs.app import jobs_app
17
- from qbraid_cli.kernels.app import kernels_app
18
- from qbraid_cli.pip.app import pip_app
13
+ from qbraid_cli.account import account_app
14
+ from qbraid_cli.admin import admin_app
15
+ from qbraid_cli.chat import ChatFormat, list_models_callback, prompt_callback
16
+ from qbraid_cli.configure import configure_app
17
+ from qbraid_cli.devices import devices_app
18
+ from qbraid_cli.files import files_app
19
+ from qbraid_cli.jobs import jobs_app
20
+ from qbraid_cli.mcp import mcp_app
21
+
22
+ try:
23
+ from qbraid_cli.envs import envs_app
24
+ from qbraid_cli.kernels import kernels_app
25
+ from qbraid_cli.pip import pip_app
26
+
27
+ ENVS_COMMANDS = True
28
+ except ImportError:
29
+ ENVS_COMMANDS = False
19
30
 
20
31
  app = typer.Typer(context_settings={"help_option_names": ["-h", "--help"]})
21
32
 
22
33
  app.add_typer(admin_app, name="admin")
23
34
  app.add_typer(configure_app, name="configure")
24
- app.add_typer(credits_app, name="credits")
35
+ app.add_typer(account_app, name="account")
25
36
  app.add_typer(devices_app, name="devices")
26
- app.add_typer(envs_app, name="envs")
37
+ app.add_typer(files_app, name="files")
27
38
  app.add_typer(jobs_app, name="jobs")
28
- app.add_typer(kernels_app, name="kernels")
29
- app.add_typer(pip_app, name="pip")
39
+ app.add_typer(mcp_app, name="mcp")
40
+
41
+ if ENVS_COMMANDS is True:
42
+ app.add_typer(envs_app, name="envs")
43
+ app.add_typer(kernels_app, name="kernels")
44
+ app.add_typer(pip_app, name="pip")
30
45
 
31
46
 
32
47
  def version_callback(value: bool):
@@ -59,7 +74,7 @@ def show_banner():
59
74
  typer.echo("")
60
75
  typer.echo("- Use 'qbraid --version' to see the current version.")
61
76
  typer.echo("")
62
- typer.echo("Reference Docs: https://docs.qbraid.com/cli/api-reference/qbraid")
77
+ rich.print("Reference Docs: https://docs.qbraid.com/cli/api-reference/qbraid")
63
78
 
64
79
 
65
80
  @app.callback(invoke_without_command=True)
@@ -79,5 +94,34 @@ def main(
79
94
  show_banner()
80
95
 
81
96
 
97
+ @app.command(help="Interact with qBraid AI chat service.", no_args_is_help=True)
98
+ def chat(
99
+ prompt: str = typer.Option(
100
+ None, "--prompt", "-p", help="The prompt to send to the chat service."
101
+ ),
102
+ model: str = typer.Option(None, "--model", "-m", help="The model to use for the chat service."),
103
+ response_format: ChatFormat = typer.Option(
104
+ ChatFormat.text, "--format", "-f", help="The format of the response."
105
+ ),
106
+ stream: bool = typer.Option(False, "--stream", "-s", help="Stream the response."),
107
+ list_models: bool = typer.Option(
108
+ False, "--list-models", "-l", help="List available chat models."
109
+ ),
110
+ ):
111
+ """
112
+ Interact with qBraid AI chat service.
113
+
114
+ """
115
+ if list_models:
116
+ list_models_callback()
117
+ elif prompt:
118
+ prompt_callback(prompt, model, response_format, stream)
119
+ else:
120
+ raise click.UsageError(
121
+ "Invalid command. Please provide a prompt using --prompt "
122
+ "or use --list-models to view available models."
123
+ )
124
+
125
+
82
126
  if __name__ == "__main__":
83
127
  app()
@@ -0,0 +1,10 @@
1
+ # Copyright (c) 2025, qBraid Development Team
2
+ # All rights reserved.
3
+
4
+ """
5
+ Module for qBraid MCP (Model Context Protocol) aggregator commands.
6
+
7
+ """
8
+ from .app import mcp_app
9
+
10
+ __all__ = ["mcp_app"]
qbraid_cli/mcp/app.py ADDED
@@ -0,0 +1,126 @@
1
+ # Copyright (c) 2025, qBraid Development Team
2
+ # All rights reserved.
3
+
4
+ """
5
+ CLI commands for qBraid MCP aggregator.
6
+
7
+ """
8
+ import typer
9
+
10
+ from .serve import serve_mcp
11
+
12
+ mcp_app = typer.Typer(help="MCP (Model Context Protocol) aggregator commands")
13
+
14
+
15
+ @mcp_app.command("serve", help="Start MCP aggregator server for Claude Desktop")
16
+ def serve(
17
+ workspace: str = typer.Option(
18
+ "lab",
19
+ "--workspace",
20
+ "-w",
21
+ help="Workspace to connect to (lab, qbook, etc.)",
22
+ ),
23
+ include_staging: bool = typer.Option(
24
+ False,
25
+ "--staging",
26
+ "-s",
27
+ help="Include staging endpoints",
28
+ ),
29
+ debug: bool = typer.Option(
30
+ False,
31
+ "--debug",
32
+ "-d",
33
+ help="Enable debug logging",
34
+ ),
35
+ ):
36
+ """
37
+ Start the qBraid MCP aggregator server.
38
+
39
+ This command starts a unified MCP server that connects to multiple qBraid MCP backends
40
+ (Lab pod_mcp, devices, jobs, etc.) and exposes them through a single stdio interface
41
+ for Claude Desktop and other AI agents.
42
+
43
+ Usage:
44
+ # Start MCP server for Lab workspace
45
+ qbraid mcp serve
46
+
47
+ # Include staging endpoints for testing
48
+ qbraid mcp serve --staging
49
+
50
+ # Enable debug logging
51
+ qbraid mcp serve --debug
52
+
53
+ Claude Desktop Configuration:
54
+ {
55
+ "mcpServers": {
56
+ "qbraid": {
57
+ "command": "qbraid",
58
+ "args": ["mcp", "serve"]
59
+ }
60
+ }
61
+ }
62
+ """
63
+ serve_mcp(workspace=workspace, include_staging=include_staging, debug=debug)
64
+
65
+
66
+ @mcp_app.command("status", help="Show status of MCP connections")
67
+ def status():
68
+ """
69
+ Show the status of MCP backend connections.
70
+
71
+ Displays which backends are configured and their connection status.
72
+ """
73
+ typer.echo("MCP Status:")
74
+ typer.echo(" Implementation in progress...")
75
+ typer.echo(" This will show connection status for all configured MCP backends")
76
+
77
+
78
+ @mcp_app.command("list", help="List available MCP servers")
79
+ def list_servers(
80
+ workspace: str = typer.Option(
81
+ None,
82
+ "--workspace",
83
+ "-w",
84
+ help="Filter by workspace (lab, qbook, etc.)",
85
+ ),
86
+ include_staging: bool = typer.Option(
87
+ False,
88
+ "--staging",
89
+ "-s",
90
+ help="Include staging endpoints",
91
+ ),
92
+ ):
93
+ """
94
+ List available qBraid MCP servers.
95
+
96
+ Shows all discovered MCP endpoints that can be connected to.
97
+ """
98
+ try:
99
+ from qbraid_core.services.mcp import discover_mcp_servers
100
+
101
+ endpoints = discover_mcp_servers(
102
+ workspace=workspace or "lab", include_staging=include_staging
103
+ )
104
+
105
+ if not endpoints:
106
+ typer.echo("No MCP servers found")
107
+ return
108
+
109
+ typer.echo(f"Found {len(endpoints)} MCP server(s):\n")
110
+ for endpoint in endpoints:
111
+ typer.echo(f" • {endpoint.name}")
112
+ typer.echo(f" {endpoint.base_url}")
113
+ if endpoint.description:
114
+ typer.echo(f" {endpoint.description}")
115
+ typer.echo()
116
+
117
+ except ImportError as exc:
118
+ typer.secho(
119
+ "Error: qbraid-core MCP module not found. "
120
+ "Please install qbraid-core with MCP support: pip install qbraid-core[mcp]",
121
+ fg=typer.colors.RED,
122
+ )
123
+ raise typer.Exit(1) from exc
124
+ except Exception as err:
125
+ typer.secho(f"Error listing MCP servers: {err}", fg=typer.colors.RED)
126
+ raise typer.Exit(1)