Skip to content

Commit 2d90908

Browse files
committed
testing new approach
1 parent e579430 commit 2d90908

File tree

3 files changed

+209
-66
lines changed

3 files changed

+209
-66
lines changed

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ dependencies = [
1414
"pyjwt>=2.4.0",
1515
"click>=8.0.0",
1616
"toml>=0.10; python_version < '3.11'",
17-
"fastmcp",
18-
"posit-sdk"
17+
"fastmcp"
1918
]
2019

2120
dynamic = ["version"]

rsconnect/main.py

Lines changed: 101 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import functools
44
import json
55
import os
6-
import subprocess
76
import sys
87
import textwrap
98
import traceback
@@ -14,7 +13,6 @@
1413
Callable,
1514
Dict,
1615
ItemsView,
17-
List,
1816
Literal,
1917
Optional,
2018
Sequence,
@@ -420,80 +418,119 @@ def version():
420418
def mcp_server(server: str, api_key: str):
421419
from fastmcp import FastMCP
422420
from fastmcp.exceptions import ToolError
423-
from posit.connect import Client
424421

425422
mcp = FastMCP("Connect MCP")
426423

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)
446427

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+
}
454465

455466
@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]
460470
) -> 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+
)
462492

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]
465495

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"]
468498

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))
471505

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
474511

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+
}
497534

498535
mcp.run()
499536

rsconnect/mcp_deploy_discovery.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""
2+
Programmatically discover all parameters for rsconnect deploy commands.
3+
This helps MCP tools understand exactly how to use `rsconnect deploy ...`
4+
"""
5+
6+
from typing import Any, Dict
7+
8+
import click
9+
10+
11+
def extract_parameter_info(param: click.Parameter) -> Dict[str, Any]:
12+
"""Extract detailed information from a Click parameter."""
13+
info: Dict[str, Any] = {
14+
"type": "string", # default
15+
}
16+
17+
if isinstance(param, click.Option) and param.opts:
18+
# Use the longest option name (usually the full form without dashes)
19+
mcp_arg_name = max(param.opts, key=len).lstrip('-').replace('-', '_')
20+
info["name"] = mcp_arg_name
21+
info["cli_flags"] = param.opts
22+
info["param_type"] = "option"
23+
else:
24+
info["name"] = param.name
25+
if isinstance(param, click.Argument):
26+
info["param_type"] = "argument"
27+
28+
# extract help text for added context
29+
help_text = getattr(param, 'help', None)
30+
if help_text:
31+
info["description"] = help_text
32+
33+
if isinstance(param, click.Option):
34+
# Boolean flags
35+
if param.is_flag:
36+
info["type"] = "boolean"
37+
info["default"] = param.default or False
38+
39+
# choices
40+
elif param.type and hasattr(param.type, 'choices'):
41+
info["type"] = "string"
42+
info["choices"] = list(param.type.choices)
43+
44+
# multiple
45+
elif param.multiple:
46+
info["type"] = "array"
47+
info["items"] = {"type": "string"}
48+
49+
# files
50+
elif isinstance(param.type, click.Path):
51+
info["type"] = "string"
52+
info["format"] = "path"
53+
if param.type.exists:
54+
info["path_must_exist"] = True
55+
if param.type.file_okay and not param.type.dir_okay:
56+
info["path_type"] = "file"
57+
elif param.type.dir_okay and not param.type.file_okay:
58+
info["path_type"] = "directory"
59+
60+
# defaults (important to avoid noise in returned command)
61+
if param.default is not None and not param.is_flag:
62+
info["default"] = param.default
63+
64+
# required params
65+
info["required"] = param.required
66+
67+
return info
68+
69+
70+
def discover_deploy_commands(cli_group: click.Group) -> Dict[str, Any]:
71+
"""Discover all deploy commands and their parameters."""
72+
73+
if "deploy" not in cli_group.commands:
74+
return {"error": "deploy command group not found"}
75+
76+
deploy_group = cli_group.commands["deploy"]
77+
78+
if not isinstance(deploy_group, click.Group):
79+
return {"error": "deploy is not a command group"}
80+
81+
result = {
82+
"group_name": "deploy",
83+
"description": deploy_group.help or "Deploy content to Posit Connect, Posit Cloud, or shinyapps.io.",
84+
"commands": {}
85+
}
86+
87+
for cmd_name, command in deploy_group.commands.items():
88+
cmd_info = {
89+
"name": cmd_name,
90+
"description": command.help or command.short_help or f"Deploy {cmd_name}",
91+
"short_help": command.short_help,
92+
"parameters": []
93+
}
94+
for param in command.params:
95+
# Skip context parameters
96+
if isinstance(param, click.Context):
97+
continue
98+
99+
if param.name in ["verbose", "v"]:
100+
continue
101+
102+
param_info = extract_parameter_info(param)
103+
cmd_info["parameters"].append(param_info)
104+
105+
result["commands"][cmd_name] = cmd_info
106+
107+
return result

0 commit comments

Comments
 (0)