as_mcp_app() turns an existing Shiny app into an MCP App without
rewriting it. The app's UI becomes the page the chat client shows, and
its server function keeps running in R: each time the app opens in the
conversation, shinymcp starts a session for it, and the user's changes
flow to that session the way they would from a browser. Reactive
expressions, observers, updateSelectInput() and friends,
validate()/req(), notifications, modals, and downloads all work.
The model gets one tool, named after the app. Its arguments are the app's inputs, so the model can open the app already set up ("show the penguins explorer for Gentoo"). The result tells the model what the app shows: text outputs as text, tables (DT's included) as their rows, plots as images.
Choose what the model sees with bindMcp(). Once any input or output is
marked, only marked inputs become tool arguments and only marked outputs
are reported; the user still sees the whole app. Buttons are never
pressed by the model unless you mark them, and password and file inputs
are never the model's to set.
Usage
as_mcp_app(x, ...)
# S3 method for class 'shiny.appobj'
as_mcp_app(
x,
name = NULL,
title = NULL,
description = NULL,
tools = NULL,
tool_name = NULL,
selective = NULL,
version = "0.1.0",
...
)
# S3 method for class 'McpApp'
as_mcp_app(x, ...)
# S3 method for class 'character'
as_mcp_app(x, name = NULL, ...)
# Default S3 method
as_mcp_app(x, ...)Arguments
- x
A Shiny app (from
shiny::shinyApp()orshiny::shinyAppDir()), a path to an app directory (withapp.R, orui.Randserver.R), or an McpApp (returned unchanged). Anapp.Rmay build an McpApp instead of a Shiny app; its tools run, and its page is built, in the app's directory.- ...
Passed on to
mcp_app(), for examplecsp,prefers_border,images, orwww(which defaults to the app directory'swww/folder).- name
App name, used for the
ui://<name>resource and the tool name. Defaults to the directory name for a path, otherwise"shiny-app".- title
Human-readable title.
- description
What the app does, for the model. By default shinymcp writes one from the inputs and outputs; a sentence about what the app is for helps the model decide when to open it.
- tools
More tools for the model, such as
ellmer::tool()objects, served next to the one that opens the app: a computation the model should be able to run without opening it, say.- tool_name
Name of the tool that opens the app. Defaults to
namewith anything other than letters, digits,-and_replaced.- selective
Whether only inputs and outputs marked with
bindMcp()are exposed to the model. Defaults toTRUEif anything is marked.- version
App version string.
Value
An McpApp.
What runs where
The page is the app's UI rendered once to HTML, with shinymcp's bridge in
place of Shiny's JavaScript. The bridge draws Shiny's built-in inputs
itself (select, slider, date, checkbox group, and so on), and puts
outputs sent back from R on the page: text, HTML, tables, plots,
renderUI(), and htmlwidgets such as plotly, DT, and leaflet.
Conditional panels show and hide, and clicks and brushes on plots reach
the server as they would from a browser, for shiny::nearPoints() and
shiny::brushedPoints().
Packages written for Shiny's JavaScript API work too. The page provides
window.Shiny with the parts packages use: input bindings they register
(shinyWidgets, for example), Shiny.setInputValue() (DT row selection,
plotly's event_data(), leaflet clicks), custom message handlers
(shinyjs), and the shiny:value family of events (shinycssloaders).
File inputs upload to the session, within shiny.maxRequestSize.
invalidateLater() and reactivePoll() run while the app is open.
The app starts as shiny::runApp() would start it: its onStart
(for a directory, global.R and the files in R/) runs once, before
the UI is built, and its code runs in the app's directory. Scripts, stylesheets, and images the
UI loads from www/ or from shiny::addResourcePath() paths are
written into the page. onStop runs when the server stops.
Sessions
Sessions live in the R process that serves the app, up to 50 at a time
(the shinymcp.max_views option), each closing after an hour without
use (shinymcp.view_timeout, in seconds). If a request reaches a
process that doesn't have the view's session (after a restart, or on a
server running several processes), a new session starts from the inputs
on the page. Anything the server function kept outside its inputs (a
reactiveVal() that counts clicks, say) starts over.
Shiny's own MCP support
Shiny is gaining MCP support of its own
(https://github.com/rstudio/shiny/pull/4407). Once it is released, it
will be the way to put a live Shiny app in a chat, and shinymcp will stop
serving live apps. as_mcp_app(), mcp_endpoint(), and
mcp_tool_module() will take only apps built from tools, and bindMcp()
and the helpers for server functions (mcp_model_context(),
mcp_host_context(), and the rest) will be removed. For something the
model should be able to use on its own, rewrite that part of the app as
tools with mcp_app(); see vignette("rewriting-as-tools").
See also
Other apps:
McpApp,
bindMcp(),
mcp_app(),
mcp_tool_module()
Examples
if (FALSE) { # \dontrun{
library(shiny)
ui <- fluidPage(
selectInput("cyl", "Cylinders", c(4, 6, 8)),
plotOutput("scatter"),
textOutput("count")
)
server <- function(input, output, session) {
cars <- reactive(mtcars[mtcars$cyl == input$cyl, ])
output$scatter <- renderPlot(plot(cars()$wt, cars()$mpg))
output$count <- renderText(paste(nrow(cars()), "cars"))
}
app <- as_mcp_app(
shinyApp(ui, server),
name = "cars",
description = "Explore fuel economy in mtcars by number of cylinders."
)
preview_app(app)
serve(app)
# Or straight from a directory:
serve("path/to/my-app")
} # }
