qbraid-cli 0.10.7__py3-none-any.whl → 0.10.9a0__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.
Potentially problematic release.
This version of qbraid-cli might be problematic. Click here for more details.
- qbraid_cli/account/app.py +3 -1
- qbraid_cli/configure/actions.py +13 -6
- qbraid_cli/main.py +2 -0
- qbraid_cli/mcp/__init__.py +10 -0
- qbraid_cli/mcp/app.py +126 -0
- qbraid_cli/mcp/serve.py +321 -0
- {qbraid_cli-0.10.7.dist-info → qbraid_cli-0.10.9a0.dist-info}/METADATA +7 -3
- {qbraid_cli-0.10.7.dist-info → qbraid_cli-0.10.9a0.dist-info}/RECORD +12 -10
- qbraid_cli/_version.py +0 -21
- {qbraid_cli-0.10.7.dist-info → qbraid_cli-0.10.9a0.dist-info}/WHEEL +0 -0
- {qbraid_cli-0.10.7.dist-info → qbraid_cli-0.10.9a0.dist-info}/entry_points.txt +0 -0
- {qbraid_cli-0.10.7.dist-info → qbraid_cli-0.10.9a0.dist-info}/licenses/LICENSE +0 -0
- {qbraid_cli-0.10.7.dist-info → qbraid_cli-0.10.9a0.dist-info}/top_level.txt +0 -0
qbraid_cli/account/app.py
CHANGED
|
@@ -11,6 +11,7 @@ from typing import Any
|
|
|
11
11
|
import rich
|
|
12
12
|
import typer
|
|
13
13
|
|
|
14
|
+
from qbraid_cli.configure.actions import QBRAID_ORG_MODEL_ENABLED
|
|
14
15
|
from qbraid_cli.handlers import run_progress_task
|
|
15
16
|
|
|
16
17
|
account_app = typer.Typer(help="Manage qBraid account.", no_args_is_help=True)
|
|
@@ -51,9 +52,10 @@ def account_info():
|
|
|
51
52
|
"email": user.get("email"),
|
|
52
53
|
"joinedDate": user.get("createdAt", "Unknown"),
|
|
53
54
|
"activePlan": user.get("activePlan", "") or "Free",
|
|
54
|
-
"organization": personal_info.get("organization", "") or "qbraid",
|
|
55
55
|
"role": personal_info.get("role", "") or "guest",
|
|
56
56
|
}
|
|
57
|
+
if QBRAID_ORG_MODEL_ENABLED:
|
|
58
|
+
metadata["organization"] = personal_info.get("organization", "") or "qbraid"
|
|
57
59
|
|
|
58
60
|
return metadata
|
|
59
61
|
|
qbraid_cli/configure/actions.py
CHANGED
|
@@ -25,6 +25,8 @@ from rich.console import Console
|
|
|
25
25
|
|
|
26
26
|
from qbraid_cli.handlers import handle_filesystem_operation
|
|
27
27
|
|
|
28
|
+
QBRAID_ORG_MODEL_ENABLED = False # Set to True if organization/workspace model is enabled
|
|
29
|
+
|
|
28
30
|
|
|
29
31
|
def validate_input(key: str, value: str) -> str:
|
|
30
32
|
"""Validate the user input based on the key.
|
|
@@ -96,16 +98,21 @@ def default_action(section: str = DEFAULT_CONFIG_SECTION):
|
|
|
96
98
|
|
|
97
99
|
default_values = {
|
|
98
100
|
"url": DEFAULT_ENDPOINT_URL,
|
|
99
|
-
"organization": DEFAULT_ORGANIZATION,
|
|
100
|
-
"workspace": DEFAULT_WORKSPACE,
|
|
101
101
|
}
|
|
102
|
+
if QBRAID_ORG_MODEL_ENABLED:
|
|
103
|
+
default_values["organization"] = DEFAULT_ORGANIZATION
|
|
104
|
+
default_values["workspace"] = DEFAULT_WORKSPACE
|
|
102
105
|
|
|
103
106
|
config[section]["url"] = prompt_for_config(config, section, "url", default_values)
|
|
104
107
|
config[section]["api-key"] = prompt_for_config(config, section, "api-key", default_values)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
|
|
109
|
+
if QBRAID_ORG_MODEL_ENABLED:
|
|
110
|
+
config[section]["organization"] = prompt_for_config(
|
|
111
|
+
config, section, "organization", default_values
|
|
112
|
+
)
|
|
113
|
+
config[section]["workspace"] = prompt_for_config(
|
|
114
|
+
config, section, "workspace", default_values
|
|
115
|
+
)
|
|
109
116
|
|
|
110
117
|
for key in list(config[section]):
|
|
111
118
|
if not config[section][key]:
|
qbraid_cli/main.py
CHANGED
|
@@ -17,6 +17,7 @@ from qbraid_cli.configure import configure_app
|
|
|
17
17
|
from qbraid_cli.devices import devices_app
|
|
18
18
|
from qbraid_cli.files import files_app
|
|
19
19
|
from qbraid_cli.jobs import jobs_app
|
|
20
|
+
from qbraid_cli.mcp import mcp_app
|
|
20
21
|
|
|
21
22
|
try:
|
|
22
23
|
from qbraid_cli.envs import envs_app
|
|
@@ -35,6 +36,7 @@ app.add_typer(account_app, name="account")
|
|
|
35
36
|
app.add_typer(devices_app, name="devices")
|
|
36
37
|
app.add_typer(files_app, name="files")
|
|
37
38
|
app.add_typer(jobs_app, name="jobs")
|
|
39
|
+
app.add_typer(mcp_app, name="mcp")
|
|
38
40
|
|
|
39
41
|
if ENVS_COMMANDS is True:
|
|
40
42
|
app.add_typer(envs_app, name="envs")
|
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)
|
qbraid_cli/mcp/serve.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# Copyright (c) 2025, qBraid Development Team
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
MCP aggregator server implementation.
|
|
6
|
+
|
|
7
|
+
Provides stdio-based MCP server that aggregates multiple qBraid MCP backends.
|
|
8
|
+
"""
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import signal
|
|
13
|
+
import sys
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
import typer
|
|
17
|
+
from qbraid_core.services.mcp import MCPRouter, MCPWebSocketClient, discover_mcp_servers
|
|
18
|
+
from qbraid_core.sessions import QbraidSession
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def setup_logging(debug: bool = False) -> None:
|
|
22
|
+
"""
|
|
23
|
+
Configure logging for MCP server.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
debug: Enable debug logging
|
|
27
|
+
"""
|
|
28
|
+
level = logging.DEBUG if debug else logging.INFO
|
|
29
|
+
|
|
30
|
+
# Log to stderr so stdout remains clean for MCP protocol
|
|
31
|
+
logging.basicConfig(
|
|
32
|
+
level=level,
|
|
33
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
34
|
+
stream=sys.stderr,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MCPAggregatorServer:
|
|
39
|
+
"""
|
|
40
|
+
MCP aggregator server that bridges stdio to multiple WebSocket backends.
|
|
41
|
+
|
|
42
|
+
Architecture:
|
|
43
|
+
Claude Desktop (stdio) <-> THIS SERVER <-> Multiple MCP WebSocket Servers
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
session: QbraidSession,
|
|
49
|
+
workspace: str = "lab",
|
|
50
|
+
include_staging: bool = False,
|
|
51
|
+
debug: bool = False,
|
|
52
|
+
):
|
|
53
|
+
"""
|
|
54
|
+
Initialize MCP aggregator server.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
session: Authenticated qBraid session
|
|
58
|
+
workspace: Workspace to connect to
|
|
59
|
+
include_staging: Include staging endpoints
|
|
60
|
+
debug: Enable debug logging
|
|
61
|
+
"""
|
|
62
|
+
self.session = session
|
|
63
|
+
self.workspace = workspace
|
|
64
|
+
self.include_staging = include_staging
|
|
65
|
+
self.debug = debug
|
|
66
|
+
self.router: Optional[MCPRouter] = None
|
|
67
|
+
self.logger = logging.getLogger(__name__)
|
|
68
|
+
self._shutdown_event = asyncio.Event()
|
|
69
|
+
|
|
70
|
+
async def initialize_backends(self) -> None:
|
|
71
|
+
"""
|
|
72
|
+
Discover and connect to MCP backend servers.
|
|
73
|
+
"""
|
|
74
|
+
# Get user info for building WebSocket URLs
|
|
75
|
+
try:
|
|
76
|
+
user_info = self.session.get_user()
|
|
77
|
+
username = user_info.get("email")
|
|
78
|
+
if not username:
|
|
79
|
+
raise ValueError("User email not found in session")
|
|
80
|
+
except Exception as err:
|
|
81
|
+
self.logger.error("Failed to get user info: %s", err)
|
|
82
|
+
typer.secho(
|
|
83
|
+
"Error: Could not authenticate. Please run 'qbraid configure' first.",
|
|
84
|
+
fg=typer.colors.RED,
|
|
85
|
+
err=True,
|
|
86
|
+
)
|
|
87
|
+
raise typer.Exit(1)
|
|
88
|
+
|
|
89
|
+
# Discover available MCP endpoints
|
|
90
|
+
endpoints = discover_mcp_servers(
|
|
91
|
+
workspace=self.workspace,
|
|
92
|
+
include_staging=self.include_staging,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if not endpoints:
|
|
96
|
+
typer.secho(
|
|
97
|
+
f"Warning: No MCP endpoints found for workspace '{self.workspace}'",
|
|
98
|
+
fg=typer.colors.YELLOW,
|
|
99
|
+
err=True,
|
|
100
|
+
)
|
|
101
|
+
raise typer.Exit(1)
|
|
102
|
+
|
|
103
|
+
self.logger.info("Discovered %d MCP endpoint(s)", len(endpoints))
|
|
104
|
+
|
|
105
|
+
# Create router
|
|
106
|
+
self.router = MCPRouter(on_message=self._handle_backend_message)
|
|
107
|
+
|
|
108
|
+
# Create WebSocket clients for each endpoint
|
|
109
|
+
for endpoint in endpoints:
|
|
110
|
+
# Use JupyterHub token for lab endpoints, API key for other endpoints
|
|
111
|
+
try:
|
|
112
|
+
if endpoint.name.startswith("lab"):
|
|
113
|
+
try:
|
|
114
|
+
token_data = self.session.get_jupyter_token_data()
|
|
115
|
+
token = token_data.get("token")
|
|
116
|
+
if not token:
|
|
117
|
+
raise ValueError("Token not found in response data")
|
|
118
|
+
except Exception as err:
|
|
119
|
+
self.logger.error(
|
|
120
|
+
"Failed to get Jupyter token for endpoint '%s': %s",
|
|
121
|
+
endpoint.name,
|
|
122
|
+
err,
|
|
123
|
+
)
|
|
124
|
+
typer.secho(
|
|
125
|
+
f"Error: Could not retrieve Jupyter token for '{endpoint.name}'. "
|
|
126
|
+
"Please ensure you are authenticated with qBraid Lab.",
|
|
127
|
+
fg=typer.colors.RED,
|
|
128
|
+
err=True,
|
|
129
|
+
)
|
|
130
|
+
raise typer.Exit(1)
|
|
131
|
+
else:
|
|
132
|
+
token = self.session.api_key
|
|
133
|
+
if not token:
|
|
134
|
+
self.logger.error("No API key available for endpoint '%s'", endpoint.name)
|
|
135
|
+
typer.secho(
|
|
136
|
+
f"Error: No API key available for '{endpoint.name}'. "
|
|
137
|
+
"Please run 'qbraid configure' to set up your credentials.",
|
|
138
|
+
fg=typer.colors.RED,
|
|
139
|
+
err=True,
|
|
140
|
+
)
|
|
141
|
+
raise typer.Exit(1)
|
|
142
|
+
|
|
143
|
+
ws_url = endpoint.build_url(username, token)
|
|
144
|
+
self.logger.info("Configuring backend: %s", endpoint.name)
|
|
145
|
+
|
|
146
|
+
client = MCPWebSocketClient(
|
|
147
|
+
websocket_url=ws_url,
|
|
148
|
+
on_message=self._handle_backend_message,
|
|
149
|
+
name=endpoint.name,
|
|
150
|
+
)
|
|
151
|
+
self.router.add_backend(endpoint.name, client)
|
|
152
|
+
except typer.Exit:
|
|
153
|
+
raise # Re-raise typer.Exit to propagate clean exits
|
|
154
|
+
except Exception as err: # pylint: disable=broad-exception-caught
|
|
155
|
+
self.logger.error("Failed to configure backend '%s': %s", endpoint.name, err)
|
|
156
|
+
typer.secho(
|
|
157
|
+
f"Error: Failed to configure backend '{endpoint.name}': {err}",
|
|
158
|
+
fg=typer.colors.RED,
|
|
159
|
+
err=True,
|
|
160
|
+
)
|
|
161
|
+
raise typer.Exit(1)
|
|
162
|
+
|
|
163
|
+
# Connect to all backends
|
|
164
|
+
self.logger.info("Connecting to backends...")
|
|
165
|
+
await self.router.connect_all()
|
|
166
|
+
|
|
167
|
+
# Check connection status
|
|
168
|
+
connected = self.router.get_connected_backends()
|
|
169
|
+
if not connected:
|
|
170
|
+
typer.secho(
|
|
171
|
+
"Error: Failed to connect to any MCP backends",
|
|
172
|
+
fg=typer.colors.RED,
|
|
173
|
+
err=True,
|
|
174
|
+
)
|
|
175
|
+
raise typer.Exit(1)
|
|
176
|
+
|
|
177
|
+
self.logger.info("Connected to %d backend(s): %s", len(connected), ", ".join(connected))
|
|
178
|
+
typer.secho(
|
|
179
|
+
f"MCP aggregator ready ({len(connected)} backend(s) connected)",
|
|
180
|
+
fg=typer.colors.GREEN,
|
|
181
|
+
err=True,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def _handle_backend_message(self, message: dict) -> None:
|
|
185
|
+
"""
|
|
186
|
+
Handle messages from backend MCP servers.
|
|
187
|
+
|
|
188
|
+
Forwards messages to stdout for Claude Desktop.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
message: Message dictionary from backend
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
self.logger.info(
|
|
195
|
+
"📥 _handle_backend_message called with message: %s", str(message)[:100]
|
|
196
|
+
)
|
|
197
|
+
# Write message to stdout as JSON
|
|
198
|
+
json_str = json.dumps(message)
|
|
199
|
+
self.logger.info("📤 Writing to stdout: %s", json_str[:100])
|
|
200
|
+
sys.stdout.write(json_str + "\n")
|
|
201
|
+
sys.stdout.flush()
|
|
202
|
+
self.logger.info("✅ Successfully sent to client")
|
|
203
|
+
except Exception as err: # pylint: disable=broad-exception-caught
|
|
204
|
+
self.logger.error("❌ Error forwarding message: %s", err, exc_info=True)
|
|
205
|
+
|
|
206
|
+
async def _stdin_loop(self) -> None:
|
|
207
|
+
"""
|
|
208
|
+
Read messages from stdin (Claude Desktop) and route to backends.
|
|
209
|
+
"""
|
|
210
|
+
loop = asyncio.get_event_loop()
|
|
211
|
+
|
|
212
|
+
self.logger.info("Starting stdin loop...")
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
while not self._shutdown_event.is_set():
|
|
216
|
+
# Read line from stdin (non-blocking)
|
|
217
|
+
try:
|
|
218
|
+
line = await loop.run_in_executor(None, sys.stdin.readline)
|
|
219
|
+
if not line:
|
|
220
|
+
# EOF reached (stdin closed)
|
|
221
|
+
self.logger.info("stdin closed, shutting down...")
|
|
222
|
+
break
|
|
223
|
+
|
|
224
|
+
line = line.strip()
|
|
225
|
+
if not line:
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
self.logger.debug("Received from client: %s...", line[:100])
|
|
229
|
+
|
|
230
|
+
# Parse JSON message
|
|
231
|
+
message = json.loads(line)
|
|
232
|
+
|
|
233
|
+
# Route to appropriate backend
|
|
234
|
+
if self.router:
|
|
235
|
+
await self.router.handle_message(message)
|
|
236
|
+
|
|
237
|
+
except json.JSONDecodeError as err:
|
|
238
|
+
self.logger.error("Invalid JSON from client: %s", err)
|
|
239
|
+
except Exception as err: # pylint: disable=broad-exception-caught
|
|
240
|
+
self.logger.error("Error processing message: %s", err)
|
|
241
|
+
|
|
242
|
+
except asyncio.CancelledError:
|
|
243
|
+
self.logger.info("stdin loop cancelled")
|
|
244
|
+
except Exception as err: # pylint: disable=broad-exception-caught
|
|
245
|
+
self.logger.error("Fatal error in stdin loop: %s", err)
|
|
246
|
+
finally:
|
|
247
|
+
self._shutdown_event.set()
|
|
248
|
+
|
|
249
|
+
async def run(self) -> None:
|
|
250
|
+
"""
|
|
251
|
+
Run the MCP aggregator server.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
# Setup signal handlers for graceful shutdown
|
|
255
|
+
def signal_handler(signum, _frame): # pylint: disable=unused-argument
|
|
256
|
+
self.logger.info("Received signal %d, shutting down...", signum)
|
|
257
|
+
self._shutdown_event.set()
|
|
258
|
+
|
|
259
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
260
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
261
|
+
|
|
262
|
+
# Start backend initialization and stdin loop concurrently
|
|
263
|
+
# This allows the server to respond to Claude's initialize request
|
|
264
|
+
# while backends are still connecting in the background
|
|
265
|
+
try:
|
|
266
|
+
await asyncio.gather(
|
|
267
|
+
self.initialize_backends(),
|
|
268
|
+
self._stdin_loop(),
|
|
269
|
+
)
|
|
270
|
+
finally:
|
|
271
|
+
# Cleanup
|
|
272
|
+
self.logger.info("Shutting down backends...")
|
|
273
|
+
if self.router:
|
|
274
|
+
await self.router.shutdown_all()
|
|
275
|
+
self.logger.info("Shutdown complete")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def serve_mcp(workspace: str, include_staging: bool, debug: bool) -> None:
|
|
279
|
+
"""
|
|
280
|
+
Start the qBraid MCP aggregator server.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
workspace: Workspace name (lab, qbook, etc.)
|
|
284
|
+
include_staging: Include staging endpoints
|
|
285
|
+
debug: Enable debug logging
|
|
286
|
+
"""
|
|
287
|
+
# Setup logging
|
|
288
|
+
setup_logging(debug=debug)
|
|
289
|
+
logger = logging.getLogger(__name__)
|
|
290
|
+
|
|
291
|
+
logger.info("Starting qBraid MCP aggregator...")
|
|
292
|
+
logger.info("Workspace: %s, Staging: %s, Debug: %s", workspace, include_staging, debug)
|
|
293
|
+
|
|
294
|
+
# Create qBraid session
|
|
295
|
+
try:
|
|
296
|
+
session = QbraidSession()
|
|
297
|
+
except Exception as err:
|
|
298
|
+
typer.secho(
|
|
299
|
+
f"Error creating qBraid session: {err}\n"
|
|
300
|
+
"Please run 'qbraid configure' to set up your credentials.",
|
|
301
|
+
fg=typer.colors.RED,
|
|
302
|
+
err=True,
|
|
303
|
+
)
|
|
304
|
+
raise typer.Exit(1)
|
|
305
|
+
|
|
306
|
+
# Create and run server
|
|
307
|
+
server = MCPAggregatorServer(
|
|
308
|
+
session=session,
|
|
309
|
+
workspace=workspace,
|
|
310
|
+
include_staging=include_staging,
|
|
311
|
+
debug=debug,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
asyncio.run(server.run())
|
|
316
|
+
except KeyboardInterrupt:
|
|
317
|
+
logger.info("Interrupted by user")
|
|
318
|
+
except Exception as err:
|
|
319
|
+
logger.error("Fatal error: %s", err, exc_info=debug)
|
|
320
|
+
typer.secho(f"Error: {err}", fg=typer.colors.RED, err=True)
|
|
321
|
+
raise typer.Exit(1)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qbraid-cli
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.9a0
|
|
4
4
|
Summary: Command Line Interface for interacting with all parts of the qBraid platform.
|
|
5
5
|
Author-email: qBraid Development Team <contact@qbraid.com>
|
|
6
6
|
License: Proprietary
|
|
@@ -30,16 +30,19 @@ License-File: LICENSE
|
|
|
30
30
|
Requires-Dist: typer>=0.12.1
|
|
31
31
|
Requires-Dist: rich>=10.11.0
|
|
32
32
|
Requires-Dist: click<=8.1.8
|
|
33
|
-
Requires-Dist: qbraid-core<0.2,>=0.1.
|
|
33
|
+
Requires-Dist: qbraid-core<0.2,>=0.1.45
|
|
34
34
|
Provides-Extra: jobs
|
|
35
35
|
Requires-Dist: amazon-braket-sdk>=1.48.1; extra == "jobs"
|
|
36
36
|
Provides-Extra: envs
|
|
37
|
-
Requires-Dist: qbraid-core[environments]<0.2,>=0.1.
|
|
37
|
+
Requires-Dist: qbraid-core[environments]<0.2,>=0.1.42; extra == "envs"
|
|
38
|
+
Provides-Extra: mcp
|
|
39
|
+
Requires-Dist: qbraid-core[mcp]<0.2,>=0.1.45; extra == "mcp"
|
|
38
40
|
Provides-Extra: dev
|
|
39
41
|
Requires-Dist: isort; extra == "dev"
|
|
40
42
|
Requires-Dist: black; extra == "dev"
|
|
41
43
|
Requires-Dist: pytest; extra == "dev"
|
|
42
44
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
45
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
43
46
|
Dynamic: license-file
|
|
44
47
|
|
|
45
48
|
<img width="full" alt="qbraid_cli" src="https://qbraid-static.s3.amazonaws.com/logos/qbraid-cli-banner.png">
|
|
@@ -165,6 +168,7 @@ Commands
|
|
|
165
168
|
files Manage qBraid cloud storage files.
|
|
166
169
|
jobs Manage qBraid quantum jobs.
|
|
167
170
|
kernels Manage qBraid kernels.
|
|
171
|
+
mcp MCP (Model Context Protocol) aggregator commands.
|
|
168
172
|
pip Run pip command in active qBraid environment.
|
|
169
173
|
```
|
|
170
174
|
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
qbraid_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
qbraid_cli/_version.py,sha256=MZArT0XdOyz4PClOVaSsIkigIviF4cY9W9HHSg6r2c8,513
|
|
3
2
|
qbraid_cli/exceptions.py,sha256=KjlhYJhSHMVazaNiBjD_Ur06w4sekP8zRsFzBdyIpno,672
|
|
4
3
|
qbraid_cli/handlers.py,sha256=qRxrB37-n9WBYIAf63KLEAPSQ7Hfhb1qRaHgsA2TVH8,8069
|
|
5
|
-
qbraid_cli/main.py,sha256=
|
|
4
|
+
qbraid_cli/main.py,sha256=s0PA-jlebLxrFiI_mPDhioQ8JHTvMOjprFa7vbsmLII,3996
|
|
6
5
|
qbraid_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
6
|
qbraid_cli/account/__init__.py,sha256=smlpUcVkM3QEbJG0norGM7i71XBJlUGQYByswTfPnmg,181
|
|
8
|
-
qbraid_cli/account/app.py,sha256=
|
|
7
|
+
qbraid_cli/account/app.py,sha256=_On93HBaBmyizcxnArpdN6zeoA1lFMNPhya0DvBDTJ4,1956
|
|
9
8
|
qbraid_cli/admin/__init__.py,sha256=qcWD5mQEUCtr49mrUpZmk7eGDe0L_Gtc8RwZmzIXSwo,175
|
|
10
9
|
qbraid_cli/admin/app.py,sha256=i_JeyJYHT6qoJrsTuf_eippp7AG6ZJ_N6-Dsrpv2XHQ,1476
|
|
11
10
|
qbraid_cli/admin/headers.py,sha256=QWAEuOu3rqumngLlOaGTI2R2wqyqmC0gUNpRt_74pd0,10610
|
|
@@ -13,7 +12,7 @@ qbraid_cli/admin/validation.py,sha256=fhpttxupCGBk56ExQPuuQm8nMnptLLy_8sj-EjpM8g
|
|
|
13
12
|
qbraid_cli/chat/__init__.py,sha256=NO41vndEdfr0vDynNcmHFh-nhzWjnWqGm4M9parikck,258
|
|
14
13
|
qbraid_cli/chat/app.py,sha256=-YqCLGDh4ezF149xB3dfuUAQotKAklZwYp0BL3HhA90,2256
|
|
15
14
|
qbraid_cli/configure/__init__.py,sha256=YaJ74Ztz2vl3eYp8_jVBucWkXscxz7EZEIzr70OfuOM,187
|
|
16
|
-
qbraid_cli/configure/actions.py,sha256=
|
|
15
|
+
qbraid_cli/configure/actions.py,sha256=Zv-y7iGgj1fwYceMSXrurpK2PyFCIitKXPqCb4H9AZo,3818
|
|
17
16
|
qbraid_cli/configure/app.py,sha256=7UN8Bje0n_s2nDE-cHid8VwOp7gl0jjw9gldyCcZNhI,4164
|
|
18
17
|
qbraid_cli/devices/__init__.py,sha256=hiScO-px6jCL5cJj5Hbty55EUfNejTO4bmqUZuS3aqc,181
|
|
19
18
|
qbraid_cli/devices/app.py,sha256=B-cRdV092C6ACeu3C-5l2P_izpPDxvCzzAKfxO1WkaM,3224
|
|
@@ -31,12 +30,15 @@ qbraid_cli/jobs/toggle_braket.py,sha256=3AEu-Z5q4avduB-fJMyMTVTuyZXuA8m-hnvi325w
|
|
|
31
30
|
qbraid_cli/jobs/validation.py,sha256=KlkqVH1-vlNCHSayEpxzyXU86_TMN5prGfMFEoyBsFs,2971
|
|
32
31
|
qbraid_cli/kernels/__init__.py,sha256=jORS9vV17s5laQyq8gSVB18EPBImgEIbMZ1wKC094DA,181
|
|
33
32
|
qbraid_cli/kernels/app.py,sha256=n-iyWJHy7_ML6qk4pp-v_rQkGA7WfnZMG8gyiCFGO1c,2948
|
|
33
|
+
qbraid_cli/mcp/__init__.py,sha256=GwW67sQpUViWhTpd6yq-kG8HH87QmCjKO0v2Ys_8Qpw,195
|
|
34
|
+
qbraid_cli/mcp/app.py,sha256=TQnM9LI9OsgbGV6Pkp_Ri7d62dJs5voPKmG1FeweSy0,3463
|
|
35
|
+
qbraid_cli/mcp/serve.py,sha256=4LFXjuv6CQPIuskPxcFXS0qE2igv1vYmLup-u1xhIZ0,11384
|
|
34
36
|
qbraid_cli/pip/__init__.py,sha256=tJtU0rxn-ODogNh5Y4pp_BgDQXMN-3JY1QGj0OZHwjQ,169
|
|
35
37
|
qbraid_cli/pip/app.py,sha256=jkk-djductrDOJIRYfHK_7WDJ12f0zOT3MMkiZp97oM,1365
|
|
36
38
|
qbraid_cli/pip/hooks.py,sha256=jkIeev3cOd-cmaoJSdSqbmhTYCs6z1we84FMqa3ZoZw,2124
|
|
37
|
-
qbraid_cli-0.10.
|
|
38
|
-
qbraid_cli-0.10.
|
|
39
|
-
qbraid_cli-0.10.
|
|
40
|
-
qbraid_cli-0.10.
|
|
41
|
-
qbraid_cli-0.10.
|
|
42
|
-
qbraid_cli-0.10.
|
|
39
|
+
qbraid_cli-0.10.9a0.dist-info/licenses/LICENSE,sha256=3KvWfsaXBCqbZ4qwk5jN9CQXE53tQeaZTySN5a-CCgQ,2753
|
|
40
|
+
qbraid_cli-0.10.9a0.dist-info/METADATA,sha256=3qv0HRybpysRiHUy37YiksN97Q4Jg-MeAzwZH9LU8l8,7839
|
|
41
|
+
qbraid_cli-0.10.9a0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
42
|
+
qbraid_cli-0.10.9a0.dist-info/entry_points.txt,sha256=c5ZJ7NjbxhDqMpou9q5F03_b_KG34HzFDijIDmEIwgQ,47
|
|
43
|
+
qbraid_cli-0.10.9a0.dist-info/top_level.txt,sha256=LTYJgeYSCHo9Il8vZu0yIPuGdGyNaIw6iRy6BeoZo8o,11
|
|
44
|
+
qbraid_cli-0.10.9a0.dist-info/RECORD,,
|
qbraid_cli/_version.py
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# file generated by setuptools-scm
|
|
2
|
-
# don't change, don't track in version control
|
|
3
|
-
|
|
4
|
-
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
|
5
|
-
|
|
6
|
-
TYPE_CHECKING = False
|
|
7
|
-
if TYPE_CHECKING:
|
|
8
|
-
from typing import Tuple
|
|
9
|
-
from typing import Union
|
|
10
|
-
|
|
11
|
-
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
12
|
-
else:
|
|
13
|
-
VERSION_TUPLE = object
|
|
14
|
-
|
|
15
|
-
version: str
|
|
16
|
-
__version__: str
|
|
17
|
-
__version_tuple__: VERSION_TUPLE
|
|
18
|
-
version_tuple: VERSION_TUPLE
|
|
19
|
-
|
|
20
|
-
__version__ = version = '0.10.7'
|
|
21
|
-
__version_tuple__ = version_tuple = (0, 10, 7)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|