AgentSkillsCN

Convert Shiny App

转换 Shiny 应用程序

SKILL.md

Convert Shiny App to MCP App

You are converting a Shiny application into an MCP App powered by shinymcp. Follow these steps carefully.

Conversion Approach

MCP Apps replace Shiny's reactive server with AI-invoked tools. Instead of reactive() and observe(), the AI calls tools that return values which the JS bridge routes to output elements in the browser. The UI is plain HTML with data attributes; no Shiny server is required.

Step-by-Step Process

1. Read the Shiny Source

Read the entire app (either app.R or the ui.R/server.R pair). Identify:

  • All inputs (selectInput, numericInput, textInput, sliderInput, etc.)
  • All outputs (textOutput, plotOutput, tableOutput, uiOutput, etc.)
  • Reactive expressions and observers that connect inputs to outputs

2. Map Inputs to MCP Components

Shiny InputMCP Component
selectInputmcp_select(id, label, choices)
numericInputmcp_numeric_input(id, label, value, min, max)
textInputmcp_text_input(id, label)
sliderInputmcp_slider(id, label, min, max, value)
checkboxInputmcp_checkbox(id, label)
actionButtonmcp_button(id, label)
radioButtonsmcp_select(id, label, choices) (use select)
fileInputSee "File Uploads" below

3. Map Outputs to MCP Components

Shiny OutputMCP Component
textOutputmcp_text(id)
verbatimTextOutputmcp_text(id)
plotOutputmcp_plot(id)
tableOutputmcp_table(id)
htmlOutputmcp_html(id)
uiOutputmcp_html(id) (render as HTML string)

4. Convert Reactive Logic to Tools

Group related reactive chains into tools. Each tool should:

  • Accept the relevant input values as arguments
  • Perform the computation that the reactive expressions did
  • Return a named list mapping output IDs to their rendered values

Simple apps: One tool per output, or one tool that returns all outputs.

Reactive chains: If output B depends on reactive A, which depends on inputs X and Y, create a single tool that takes X and Y and returns B. Flatten the chain -- tools are stateless function calls.

Example: A Shiny app with reactive({ filter(data, col == input$x) }) feeding both a text summary and a table becomes one tool:

r
ellmer::tool(
  fun = function(x = "default") {
    filtered <- dplyr::filter(data, col == x)
    list(
      summary_text = paste(nrow(filtered), "rows"),
      data_table = render_table_html(filtered)
    )
  },
  name = "filter_data",
  description = "Filter data by column value and return summary and table",
  arguments = list(
    x = ellmer::type_string("Column filter value")
  )
)

5. Assemble the MCP App

r
library(shinymcp)

ui <- htmltools::tagList(
  # Inputs
  mcp_select("x", "Filter by:", choices),
  # Outputs
  mcp_text("summary_text"),
  mcp_table("data_table")
)

tools <- list(filter_data_tool)

app <- mcp_app(ui, tools, name = "my-app")
serve(app)

Special Cases

Dynamic UI (uiOutput / renderUI)

MCP Apps do not support dynamic UI generation from the server. Instead:

  • Use mcp_html(id) and return rendered HTML strings from tools
  • For conditional visibility, return empty strings when hidden
  • For dynamic choices, use a fixed mcp_select and document valid choices in the tool description

File Uploads

MCP Apps run inside an AI tool-use context and cannot handle file uploads the way Shiny does. Alternatives:

  • Accept file paths as text input (mcp_text_input("file_path", "File path:"))
  • Have the tool read from a known directory
  • Use mcp_text_input for pasting data directly

Shiny Modules

Flatten modules into the top-level app. Each module's server logic becomes one or more tools. Prefix tool names with the module name for clarity:

  • mod_chart_server -> tool named chart_update
  • mod_filter_server -> tool named filter_apply

Plots

For plotOutput, use mcp_plot(id) and return a base64-encoded PNG from the tool. The bridge wraps it in an <img> tag automatically. Example:

r
fun = function(...) {
  tmp <- tempfile(fileext = ".png")
  grDevices::png(tmp, width = 600, height = 400, res = 96)
  plot(...)
  grDevices::dev.off()
  on.exit(unlink(tmp))
  base64enc::base64encode(tmp)
}

For ggplot2 plots, use ggsave():

r
fun = function(...) {
  p <- ggplot2::ggplot(...) + ggplot2::geom_point()
  tmp <- tempfile(fileext = ".png")
  ggplot2::ggsave(tmp, p, width = 7, height = 4, dpi = 144, bg = "white")
  on.exit(unlink(tmp))
  base64enc::base64encode(tmp)
}

When returning multiple outputs (plot + text), use a named list:

r
fun = function(...) {
  # ... generate plot_b64 and summary_text ...
  list(my_plot = plot_b64, my_text = summary_text)
}

The keys must match the output IDs in the UI (mcp_plot("my_plot"), mcp_text("my_text")). The server returns these as structuredContent and the bridge routes each value to the correct output element.

Tables

For tableOutput, use mcp_table(id) and return an HTML table string. You can use htmltools::tags$table(...) or knitr::kable(df, format = "html").

Important Notes

  • The JS bridge handles all input-to-tool-to-output communication automatically
  • Tools are stateless -- do not rely on global mutable state between calls
  • Keep tool argument types simple (strings, numbers, booleans)
  • Provide clear descriptions for each tool argument so the AI knows what to pass
  • Test the converted app with serve(app) to verify it works