|
3 | 3 | import functools |
4 | 4 | import json |
5 | 5 | import os |
6 | | -import shlex |
7 | 6 | import sys |
8 | 7 | import textwrap |
9 | 8 | import traceback |
@@ -405,70 +404,82 @@ def version(): |
405 | 404 |
|
406 | 405 | @cli.command(help="Start the MCP server") |
407 | 406 | def mcp_server(): |
408 | | - import subprocess |
409 | | - |
410 | 407 | from fastmcp import FastMCP |
411 | 408 | from fastmcp.exceptions import ToolError |
412 | 409 |
|
413 | 410 | mcp = FastMCP("Connect MCP") |
414 | 411 |
|
415 | | - # Discover all deploy commands at startup |
416 | | - from .mcp_deploy_context import discover_deploy_commands |
417 | | - deploy_commands_info = discover_deploy_commands(cli) |
418 | | - |
419 | | - @mcp.tool() |
420 | | - def list_servers(): |
421 | | - """Show the stored information about each known server nickname.""" |
422 | | - try: |
423 | | - result = subprocess.run(["rsconnect", "list"], capture_output=True, text=True, check=True) |
424 | | - return result.stdout |
425 | | - except subprocess.CalledProcessError as e: |
426 | | - raise ToolError(f"Command failed with error: {e}") |
427 | | - |
428 | | - @mcp.tool() |
429 | | - def add_server( |
430 | | - server_url: str, |
431 | | - nickname: str, |
432 | | - api_key: str, |
433 | | - ) -> Dict[str, Any]: |
434 | | - """ |
435 | | - Prompt user to add a Posit Connect server using rsconnect add. |
436 | | -
|
437 | | - This tool guides users through registering a Connect server so they can use |
438 | | - rsconnect deployment commands. The server nickname allows you to reference |
439 | | - the server in future deployment commands. |
440 | | -
|
441 | | - :param server_url: the URL of your Posit Connect server (e.g., https://my.connect.server/) |
442 | | - :param nickname: a nickname you choose for the server (e.g., myServer) |
443 | | - :param api_key: your personal API key for authentication |
444 | | - :return: a dictionary containing the command to run and instructions. |
445 | | - """ |
446 | | - command = f"rsconnect add --server {server_url} --name {nickname} --api-key {api_key}" |
447 | | - |
448 | | - return { |
449 | | - "type": "command", |
450 | | - "command": command |
451 | | - } |
| 412 | + # Discover all commands at startup |
| 413 | + from .mcp_deploy_context import discover_all_commands |
| 414 | + all_commands_info = discover_all_commands(cli) |
452 | 415 |
|
453 | 416 | @mcp.tool() |
454 | | - def deploy_command_context( |
455 | | - content_type: str |
| 417 | + def get_command_info( |
| 418 | + command_path: str, |
456 | 419 | ) -> Dict[str, Any]: |
457 | 420 | """ |
458 | | - Get the parameter schema for rsconnect deploy commands that should be executed via bash. |
| 421 | + Get the parameter schema for any rsconnect command. |
459 | 422 |
|
460 | | - Returns information about the parameters needed to construct an rsconnect deploy |
461 | | - command that can be executed in a bash shell. |
| 423 | + Returns information about the parameters needed to construct an rsconnect command |
| 424 | + that can be executed in a bash shell. Supports nested command groups of arbitrary depth. |
462 | 425 |
|
463 | | - :param content_type: the type of content (e.g., 'shiny', 'notebook', 'quarto') |
| 426 | + :param command_path: space-separated command path (e.g., 'version', 'deploy notebook', 'content build add') |
464 | 427 | :return: dictionary with command parameter schema and execution metadata |
465 | 428 | """ |
466 | | - ctx = deploy_commands_info["content_type"][content_type] |
467 | | - return { |
468 | | - "context": ctx, |
469 | | - "command_usage": "rsconnect deploy COMMAND [OPTIONS] DIRECTORY [ARGS]...", |
470 | | - "shell": "bash" |
471 | | - } |
| 429 | + try: |
| 430 | + # split the command path into parts |
| 431 | + parts = command_path.strip().split() |
| 432 | + if not parts: |
| 433 | + available_commands = list(all_commands_info["commands"].keys()) |
| 434 | + return { |
| 435 | + "error": "Command path cannot be empty", |
| 436 | + "available_commands": available_commands |
| 437 | + } |
| 438 | + |
| 439 | + current_info = all_commands_info |
| 440 | + current_path = [] |
| 441 | + |
| 442 | + for _, part in enumerate(parts): |
| 443 | + # error if we find unexpected additional subcommands |
| 444 | + if "commands" not in current_info: |
| 445 | + return { |
| 446 | + "error": f"'{' '.join(current_path)}' is not a command group. Unexpected part: '{part}'", |
| 447 | + "type": "command", |
| 448 | + "command_path": f"rsconnect {' '.join(current_path)}", |
| 449 | + } |
| 450 | + |
| 451 | + # try to return useful messaging for invalid subcommands |
| 452 | + if part not in current_info["commands"]: |
| 453 | + available = list(current_info["commands"].keys()) |
| 454 | + path_str = ' '.join(current_path) if current_path else "top level" |
| 455 | + return { |
| 456 | + "error": f"Command '{part}' not found in {path_str}", |
| 457 | + "available_commands": available |
| 458 | + } |
| 459 | + |
| 460 | + current_info = current_info["commands"][part] |
| 461 | + current_path.append(part) |
| 462 | + |
| 463 | + # still return something useful if additional subcommands are needed |
| 464 | + if "commands" in current_info: |
| 465 | + return { |
| 466 | + "type": "command_group", |
| 467 | + "name": current_info.get("name", parts[-1]), |
| 468 | + "description": current_info.get("description"), |
| 469 | + "available_subcommands": list(current_info["commands"].keys()), |
| 470 | + "message": f"The '{' '.join(parts)}' command requires a subcommand." |
| 471 | + } |
| 472 | + else: |
| 473 | + return { |
| 474 | + "type": "command", |
| 475 | + "command_path": f"rsconnect {' '.join(parts)}", |
| 476 | + "name": current_info.get("name", parts[-1]), |
| 477 | + "description": current_info.get("description"), |
| 478 | + "parameters": current_info.get("parameters", []), |
| 479 | + "shell": "bash" |
| 480 | + } |
| 481 | + except Exception as e: |
| 482 | + raise ToolError(f"Failed to retrieve command info: {str(e)}") |
472 | 483 |
|
473 | 484 | mcp.run() |
474 | 485 |
|
@@ -514,7 +525,7 @@ def _test_spcs_creds(server: SPCSConnectServer): |
514 | 525 |
|
515 | 526 | @cli.command( |
516 | 527 | short_help="Create an initial admin user to bootstrap a Connect instance.", |
517 | | - help="Creates an initial admin user to bootstrap a Connect instance. Returns the provisionend API key.", |
| 528 | + help="Creates an initial admin user to bootstrap a Connect instance. Returns the provisioned API key.", |
518 | 529 | no_args_is_help=True, |
519 | 530 | ) |
520 | 531 | @click.option( |
|
0 commit comments