|
3 | 3 | import functools |
4 | 4 | import json |
5 | 5 | import os |
6 | | -import subprocess |
7 | 6 | import sys |
8 | 7 | import textwrap |
9 | 8 | import traceback |
|
14 | 13 | Callable, |
15 | 14 | Dict, |
16 | 15 | ItemsView, |
17 | | - List, |
18 | 16 | Literal, |
19 | 17 | Optional, |
20 | 18 | Sequence, |
@@ -420,80 +418,119 @@ def version(): |
420 | 418 | def mcp_server(server: str, api_key: str): |
421 | 419 | from fastmcp import FastMCP |
422 | 420 | from fastmcp.exceptions import ToolError |
423 | | - from posit.connect import Client |
424 | 421 |
|
425 | 422 | mcp = FastMCP("Connect MCP") |
426 | 423 |
|
427 | | - def get_content_logs(app_guid: str): |
428 | | - try: |
429 | | - client = Client(server, api_key) |
430 | | - response = client.get(f"v1/content/{app_guid}/jobs") |
431 | | - jobs = response.json() |
432 | | - # first job key is the most recent one |
433 | | - key = jobs[0]["key"] |
434 | | - logs = client.get(f"v1/content/{app_guid}/jobs/{key}/log") |
435 | | - return logs.json() |
436 | | - except Exception as e: |
437 | | - raise ToolError(f"Failed to get logs: {e}") |
438 | | - |
439 | | - def list_content(): |
440 | | - try: |
441 | | - client = Client(server, api_key) |
442 | | - response = client.get("v1/content") |
443 | | - return response.json() |
444 | | - except Exception as e: |
445 | | - raise ToolError(f"Failed to list content: {e}") |
| 424 | + # Discover all deploy commands at startup |
| 425 | + from .mcp_deploy_discovery import discover_deploy_commands |
| 426 | + deploy_commands_info = discover_deploy_commands(cli) |
446 | 427 |
|
447 | | - def get_content_item(app_guid: str): |
448 | | - try: |
449 | | - client = Client(server, api_key) |
450 | | - response = client.content.get(app_guid) |
451 | | - return response |
452 | | - except Exception as e: |
453 | | - raise ToolError(f"Failed to get content: {e}") |
| 428 | + @mcp.tool() |
| 429 | + async def add_server( |
| 430 | + server_url: str, |
| 431 | + nickname: str, |
| 432 | + api_key: str, |
| 433 | + ) -> Dict[str, Any]: |
| 434 | + """Prompt user to add a Posit Connect server using rsconnect add. |
| 435 | +
|
| 436 | + This tool guides users through registering a Connect server so they can use |
| 437 | + rsconnect deployment commands. The server nickname allows you to reference |
| 438 | + the server in future deployment commands. |
| 439 | +
|
| 440 | + Args: |
| 441 | + server_url: The URL of your Posit Connect server (e.g., https://my.connect.server/) |
| 442 | + nickname: A nickname you choose for the server (e.g., myServer) |
| 443 | + api_key: Your personal API key for authentication |
| 444 | +
|
| 445 | + Returns: |
| 446 | + A dictionary containing the command to run and instructions |
| 447 | + """ |
| 448 | + command = f"rsconnect add --server {server_url} --name {nickname} --api-key {api_key}" |
| 449 | + |
| 450 | + return { |
| 451 | + "command": command, |
| 452 | + "message": f"Run the following command to add your Posit Connect server:\n\n{command}\n\n" |
| 453 | + f"This will:\n" |
| 454 | + f"1. Register '{server_url}' with the nickname '{nickname}'\n" |
| 455 | + f"2. Validate the server connection and API key\n" |
| 456 | + f"3. Store the credentials securely for future deployments\n\n" |
| 457 | + f"After adding the server, you can deploy content using commands like:\n" |
| 458 | + f" rsconnect deploy shiny <directory> --name {nickname}", |
| 459 | + "next_steps": [ |
| 460 | + "Run the add command shown above", |
| 461 | + "Verify the server was added with: rsconnect list", |
| 462 | + "Deploy content using the nickname: rsconnect deploy <type> <path> --name " + nickname |
| 463 | + ] |
| 464 | + } |
454 | 465 |
|
455 | 466 | @mcp.tool() |
456 | | - async def deploy_shiny( |
457 | | - directory: str, |
458 | | - name: Optional[str] = None, |
459 | | - title: Optional[str] = None |
| 467 | + async def build_deploy_command( |
| 468 | + application_type: str, |
| 469 | + parameters: Dict[str, Any] |
460 | 470 | ) -> Dict[str, Any]: |
461 | | - """Deploy a Shiny application to Posit Connect""" |
| 471 | + """Build a deploy command for any content type using discovered parameters. |
| 472 | +
|
| 473 | + This tool automatically builds the correct rsconnect deploy command |
| 474 | + with proper parameter names and types based on the command type. |
| 475 | +
|
| 476 | + Args: |
| 477 | + command_type: The type of content to deploy (e.g., 'shiny', 'notebook', 'quarto', etc.) |
| 478 | + parameters: Dictionary of parameter names and their values |
| 479 | +
|
| 480 | + Returns: |
| 481 | + Dictionary with the generated command and explanation |
| 482 | + """ |
| 483 | + # Use the deploy commands info discovered at startup |
| 484 | + deploy_info = deploy_commands_info |
| 485 | + |
| 486 | + if application_type not in deploy_info["commands"]: |
| 487 | + available = ", ".join(deploy_info["commands"].keys()) |
| 488 | + raise ToolError( |
| 489 | + f"Unknown application type '{application_type}'. " |
| 490 | + f"Available types: {available}" |
| 491 | + ) |
462 | 492 |
|
463 | | - # Build the CLI command |
464 | | - args = ["deploy", "shiny", directory] |
| 493 | + cmd_parts = ["rsconnect", "deploy", application_type] |
| 494 | + cmd_info = deploy_info["commands"][application_type] |
465 | 495 |
|
466 | | - if name: |
467 | | - args.extend(["--name", name]) |
| 496 | + arguments = [p for p in cmd_info["parameters"] if p.get("param_type") == "argument"] |
| 497 | + options = [p for p in cmd_info["parameters"] if p.get("param_type") == "option"] |
468 | 498 |
|
469 | | - if title: |
470 | | - args.extend(["--title", title]) |
| 499 | + # add arguments |
| 500 | + for arg in arguments: |
| 501 | + if arg["name"] in parameters: |
| 502 | + value = parameters[arg["name"]] |
| 503 | + if value: |
| 504 | + cmd_parts.append(str(value)) |
471 | 505 |
|
472 | | - args.extend(["--server", server]) |
473 | | - args.extend(["--api-key", api_key]) |
| 506 | + # add options |
| 507 | + for opt in options: |
| 508 | + param_name = opt["name"] |
| 509 | + if param_name not in parameters: |
| 510 | + continue |
474 | 511 |
|
475 | | - try: |
476 | | - result = subprocess.run( |
477 | | - args, |
478 | | - check=True, |
479 | | - capture_output=True, |
480 | | - text=True |
481 | | - ) |
482 | | - return { |
483 | | - "success": True, |
484 | | - "message": "Deployment completed successfully", |
485 | | - "stdout": result.stdout, |
486 | | - "stderr": result.stderr |
487 | | - } |
488 | | - except subprocess.CalledProcessError as e: |
489 | | - raise ToolError(f"Deployment failed with exit code {e.returncode}: {e.stderr}") |
490 | | - except Exception as e: |
491 | | - raise ToolError(f"Command failed with error: {e}") |
492 | | - |
493 | | - |
494 | | - mcp.tool(description="Get content logs from Posit Connect")(get_content_logs) |
495 | | - mcp.tool(description="List content from Posit Connect")(list_content) |
496 | | - mcp.tool(description="Get content item from Posit Connect")(get_content_item) |
| 512 | + value = parameters[param_name] |
| 513 | + if value is None or value is False or value == "": |
| 514 | + continue |
| 515 | + |
| 516 | + cli_flag = max(opt.get("cli_flags", []), key=len) if opt.get("cli_flags") else f"--{param_name.replace('_', '-')}" |
| 517 | + |
| 518 | + if opt["type"] == "boolean" and value: |
| 519 | + cmd_parts.append(cli_flag) |
| 520 | + elif opt["type"] == "array" and isinstance(value, list): |
| 521 | + for item in value: |
| 522 | + cmd_parts.extend([cli_flag, str(item)]) |
| 523 | + else: |
| 524 | + cmd_parts.extend([cli_flag, str(value)]) |
| 525 | + |
| 526 | + command = " ".join(cmd_parts) |
| 527 | + |
| 528 | + return { |
| 529 | + "command": command, |
| 530 | + "application_type": application_type, |
| 531 | + "description": cmd_info["description"], |
| 532 | + "message": f"Run the following command to deploy your {application_type} content:\n\n{command}", |
| 533 | + } |
497 | 534 |
|
498 | 535 | mcp.run() |
499 | 536 |
|
|
0 commit comments