|
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, |
@@ -405,95 +403,120 @@ def version(): |
405 | 403 |
|
406 | 404 |
|
407 | 405 | @cli.command(help="Start the MCP server") |
408 | | -@click.option( |
409 | | - "--server", |
410 | | - "-s", |
411 | | - envvar="CONNECT_SERVER", |
412 | | - help="Posit Connect server URL" |
413 | | -) |
414 | | -@click.option( |
415 | | - "--api-key", |
416 | | - "-k", |
417 | | - envvar="CONNECT_API_KEY", |
418 | | - help="The API key to use to authenticate with Posit Connect." |
419 | | -) |
420 | | -def mcp_server(server: str, api_key: str): |
| 406 | +def mcp_server(): |
421 | 407 | from fastmcp import FastMCP |
422 | 408 | from fastmcp.exceptions import ToolError |
423 | | - from posit.connect import Client |
424 | 409 |
|
425 | 410 | mcp = FastMCP("Connect MCP") |
426 | 411 |
|
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}") |
| 412 | + # Discover all deploy commands at startup |
| 413 | + from .mcp_deploy_context import discover_deploy_commands |
| 414 | + deploy_commands_info = discover_deploy_commands(cli) |
446 | 415 |
|
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}") |
| 416 | + @mcp.tool() |
| 417 | + def list_servers() -> Dict[str, Any]: |
| 418 | + """ |
| 419 | + Show the stored information about each known server nickname. |
| 420 | +
|
| 421 | + :return: a dictionary containing the command to run and instructions. |
| 422 | + """ |
| 423 | + return { |
| 424 | + "type": "command", |
| 425 | + "command": "rsconnect list" |
| 426 | + } |
454 | 427 |
|
455 | 428 | @mcp.tool() |
456 | | - async def deploy_shiny( |
457 | | - directory: str, |
458 | | - name: Optional[str] = None, |
459 | | - title: Optional[str] = None |
| 429 | + def add_server( |
| 430 | + server_url: str, |
| 431 | + nickname: str, |
| 432 | + api_key: str, |
460 | 433 | ) -> Dict[str, Any]: |
461 | | - """Deploy a Shiny application to Posit Connect""" |
462 | | - |
463 | | - # Build the CLI command |
464 | | - args = ["deploy", "shiny", directory] |
| 434 | + """ |
| 435 | + Prompt user to add a Posit Connect server using rsconnect add. |
465 | 436 |
|
466 | | - if name: |
467 | | - args.extend(["--name", name]) |
| 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. |
468 | 440 |
|
469 | | - if title: |
470 | | - args.extend(["--title", title]) |
| 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}" |
471 | 447 |
|
472 | | - args.extend(["--server", server]) |
473 | | - args.extend(["--api-key", api_key]) |
| 448 | + return { |
| 449 | + "type": "command", |
| 450 | + "command": command |
| 451 | + } |
474 | 452 |
|
475 | | - try: |
476 | | - result = subprocess.run( |
477 | | - args, |
478 | | - check=True, |
479 | | - capture_output=True, |
480 | | - text=True |
| 453 | + @mcp.tool() |
| 454 | + def build_deploy_command( |
| 455 | + application_type: str, |
| 456 | + parameters: Dict[str, Any] |
| 457 | + ) -> Dict[str, Any]: |
| 458 | + """ |
| 459 | + Build a deploy command for any content type using discovered parameters. |
| 460 | +
|
| 461 | + This tool automatically builds the correct rsconnect deploy command |
| 462 | + with proper parameter names and types based on the command type. |
| 463 | +
|
| 464 | + :param application_type: the type of content to deploy (e.g., 'shiny', 'notebook', 'quarto', etc.) |
| 465 | + :param parameters: dictionary of parameter names and their values. |
| 466 | + :return: dictionary with the generated command and instructions. |
| 467 | + """ |
| 468 | + # use the deploy commands info discovered at startup |
| 469 | + deploy_info = deploy_commands_info |
| 470 | + |
| 471 | + if application_type not in deploy_info["app_type"]: |
| 472 | + available = ", ".join(deploy_info["app_type"].keys()) |
| 473 | + raise ToolError( |
| 474 | + f"Unknown application type '{application_type}'. " |
| 475 | + f"Available types: {available}" |
481 | 476 | ) |
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) |
| 477 | + |
| 478 | + cmd_parts = ["rsconnect", "deploy", application_type] |
| 479 | + cmd_info = deploy_info["app_type"][application_type] |
| 480 | + |
| 481 | + arguments = [p for p in cmd_info["parameters"] if p.get("param_type") == "argument"] |
| 482 | + options = [p for p in cmd_info["parameters"] if p.get("param_type") == "option"] |
| 483 | + |
| 484 | + # add arguments |
| 485 | + for arg in arguments: |
| 486 | + if arg["name"] in parameters: |
| 487 | + value = parameters[arg["name"]] |
| 488 | + if value is not None: |
| 489 | + cmd_parts.append(str(value)) |
| 490 | + |
| 491 | + # add options |
| 492 | + for opt in options: |
| 493 | + param_name = opt["name"] |
| 494 | + if param_name not in parameters: |
| 495 | + continue |
| 496 | + |
| 497 | + value = parameters[param_name] |
| 498 | + if value is None: |
| 499 | + continue |
| 500 | + |
| 501 | + cli_flag = max(opt.get("cli_flags", []), key=len) if opt.get("cli_flags") else f"--{param_name.replace('_', '-')}" |
| 502 | + |
| 503 | + if opt["type"] == "boolean": |
| 504 | + if value: |
| 505 | + cmd_parts.append(cli_flag) |
| 506 | + elif opt["type"] == "array" and isinstance(value, list): |
| 507 | + for item in value: |
| 508 | + cmd_parts.extend([cli_flag, str(item)]) |
| 509 | + else: |
| 510 | + cmd_parts.extend([cli_flag, str(value)]) |
| 511 | + |
| 512 | + command = " ".join(cmd_parts) |
| 513 | + |
| 514 | + return { |
| 515 | + "type": "command", |
| 516 | + "command": command, |
| 517 | + "application_type": application_type, |
| 518 | + "description": cmd_info["description"] |
| 519 | + } |
497 | 520 |
|
498 | 521 | mcp.run() |
499 | 522 |
|
|
0 commit comments