|
3 | 3 | import functools |
4 | 4 | import json |
5 | 5 | import os |
| 6 | +import subprocess |
6 | 7 | import sys |
7 | 8 | import textwrap |
8 | 9 | import traceback |
9 | 10 | from functools import wraps |
10 | 11 | from os.path import abspath, dirname, exists, isdir, join |
11 | | -from typing import Callable, ItemsView, Literal, Optional, Sequence, TypeVar, cast |
| 12 | +from typing import ( |
| 13 | + Any, |
| 14 | + Callable, |
| 15 | + Dict, |
| 16 | + ItemsView, |
| 17 | + List, |
| 18 | + Literal, |
| 19 | + Optional, |
| 20 | + Sequence, |
| 21 | + TypeVar, |
| 22 | + cast, |
| 23 | +) |
12 | 24 |
|
13 | 25 | import click |
14 | 26 |
|
@@ -392,6 +404,100 @@ def version(): |
392 | 404 | click.echo(VERSION) |
393 | 405 |
|
394 | 406 |
|
| 407 | +@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): |
| 421 | + from fastmcp import FastMCP |
| 422 | + from fastmcp.exceptions import ToolError |
| 423 | + from posit.connect import Client |
| 424 | + |
| 425 | + mcp = FastMCP("Connect MCP") |
| 426 | + |
| 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}") |
| 446 | + |
| 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}") |
| 454 | + |
| 455 | + @mcp.tool() |
| 456 | + async def deploy_shiny( |
| 457 | + directory: str, |
| 458 | + name: Optional[str] = None, |
| 459 | + title: Optional[str] = None |
| 460 | + ) -> Dict[str, Any]: |
| 461 | + """Deploy a Shiny application to Posit Connect""" |
| 462 | + |
| 463 | + # Build the CLI command |
| 464 | + args = ["rsconnect", "deploy", "shiny", directory] |
| 465 | + |
| 466 | + if name: |
| 467 | + args.extend(["--name", name]) |
| 468 | + |
| 469 | + if title: |
| 470 | + args.extend(["--title", title]) |
| 471 | + |
| 472 | + args.extend(["--server", server]) |
| 473 | + args.extend(["--api-key", api_key]) |
| 474 | + |
| 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) |
| 497 | + |
| 498 | + mcp.run() |
| 499 | + |
| 500 | + |
395 | 501 | def _test_server_and_api(server: str, api_key: str, insecure: bool, ca_cert: str | None): |
396 | 502 | """ |
397 | 503 | Test the specified server information to make sure it works. If so, a |
|
0 commit comments